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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cef8634d2f2d3e9e90289e4098b5bac3341872844764b0db4bca9e0b3bdaf0e
4
- data.tar.gz: ca753e285e70bce1f8ca7d947c56d76683901666c103765de05a6f58a36c879b
3
+ metadata.gz: a0252edadec09077b1c95f148f2e5dec18a0faf393f7c9b225fc0ca0962b06af
4
+ data.tar.gz: f45851705405c80dd7ec484c439711716146317e0b19efdba3e4d756f5fce2fa
5
5
  SHA512:
6
- metadata.gz: ac8287096aa861556171e3b7ad393640ff4193e16de0d062666f0b576b80b6768aaa96fa47e511ab8b94ce4e0473cec28c7f402b89b5c76f691c72d62e017934
7
- data.tar.gz: 9a2778e40dd3a4e189ebe32bd5e56b0676d08be1bb05f13f9c3b08378d9c9a65638b5f0742bff33991f2e2521dd4549a2b9f112a526226f71889c1fde8116616
6
+ metadata.gz: 76e14e7c6e5a94c2f4a88fe987477e8ca960aab236fb2d63b24e7655a694d5e0da57a0b08aee141c4f6b4ecd4e1ec2caf64d671703033ab626a725de30c4a9a5
7
+ data.tar.gz: 879632d0abb8539b61b99e49af8bf427b786163be9e669e0a5542f2286234666d9c183a360fe21cac97bc9debb1ad97e816b15f18bd1cb494adfb205252c43ad
data/lib/free-range.rb CHANGED
@@ -3,7 +3,6 @@ require 'rmagick'
3
3
  require 'fileutils'
4
4
 
5
5
  module FreeRange
6
-
7
6
  # Перевірка наявності ImageMagick
8
7
  begin
9
8
  require 'rmagick'
@@ -13,6 +12,60 @@ module FreeRange
13
12
  exit 1
14
13
  end
15
14
 
15
+ # Клас для зберігання конфігураційних команд
16
+ class Config
17
+ attr_accessor :username, :password
18
+
19
+ # Ініціалізує об’єкт конфігурації
20
+ # @param login [Hash] Hash with target, username, and password
21
+ # @yield [self] Yields self for block-based configuration
22
+ def initialize(login)
23
+ @login = login
24
+ @username = nil
25
+ @password = nil
26
+ yield self if block_given?
27
+ end
28
+
29
+ # Повертає команду SSH
30
+ # @return [String] SSH command string
31
+ def ssh_command
32
+ "sshpass -p \"#{@login[:password]}\" ssh -C -x -4 -o StrictHostKeyChecking=no #{@login[:username]}@#{@login[:target]}"
33
+ end
34
+
35
+ # Повертає команду для отримання даних про передплатників
36
+ # @return [String] Subscribers command string
37
+ def subscribers_command
38
+ "ssh -C -x roffice /usr/local/share/noc/bin/radius-subscribers"
39
+ end
40
+
41
+ # Повертає команду для отримання списку інтерфейсів
42
+ # @return [String] Command to fetch interfaces
43
+ def command_interfaces
44
+ 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
45
+ end
46
+
47
+ # Повертає команду для отримання діапазонів VLAN
48
+ # @param interface [String, nil] Interface name or nil for all interfaces
49
+ # @return [String] Command to fetch VLAN ranges
50
+ def command_ranges(interface = nil)
51
+ interface ? "show configuration interfaces #{interface} | no-more | display set | match dynamic-profile | match \"ranges ([0-9]+(-[0-9]+)?)\"" : 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
52
+ end
53
+
54
+ # Повертає команду для отримання демультиплексорних VLAN
55
+ # @param interface [String, nil] Interface name or nil for all interfaces
56
+ # @return [String] Command to fetch demux VLANs
57
+ def command_demux(interface = nil)
58
+ interface ? "show configuration interfaces #{interface} | display set | match unnumbered-address" : 'show configuration interfaces | display set | match unnumbered-address'
59
+ end
60
+
61
+ # Повертає команду для отримання інших VLAN
62
+ # @param interface [String, nil] Interface name or nil for all interfaces
63
+ # @return [String] Command to fetch other VLANs
64
+ def command_another(interface = nil)
65
+ interface ? "show configuration interfaces #{interface} | display set | match vlan-id" : 'show configuration interfaces | display set | match vlan-id'
66
+ end
67
+ end
68
+
16
69
  # Абстрактний клас для роботи з VLAN
17
70
  class VlanContainer
18
71
  def initialize
@@ -20,16 +73,20 @@ module FreeRange
20
73
  end
21
74
 
22
75
  # Додаємо VLAN до списку (віртуальний метод, може бути перевизначений)
76
+ # @param vlan [Integer] VLAN ID to add
77
+ # @return [void]
23
78
  def add_vlan(vlan)
24
79
  @vlans << vlan
25
80
  end
26
81
 
27
82
  # Повертаємо масив VLAN
83
+ # @return [Array<Integer>] Sorted array of unique VLAN IDs
28
84
  def vlans
29
85
  @vlans.uniq.sort
30
86
  end
31
87
 
32
88
  # Створюємо хеш діапазонів із VLAN
89
+ # @return [Hash{Integer => Integer}] Hash mapping range start to range end
33
90
  def ranges
34
91
  return {} if @vlans.empty?
35
92
 
@@ -59,16 +116,23 @@ module FreeRange
59
116
  end
60
117
 
61
118
  # Додаємо діапазон до списку VLAN-ів
119
+ # @param start [Integer] Start of VLAN range
120
+ # @param finish [Integer] End of VLAN range
121
+ # @return [void]
62
122
  def add_range(start, finish)
63
123
  (start..finish).each { |vlan| @vlans << vlan }
64
124
  end
65
125
 
66
126
  # Додаємо діапазон до списку "інших" VLAN-ів
127
+ # @param start [Integer] Start of another VLAN range
128
+ # @param finish [Integer] End of another VLAN range
129
+ # @return [void]
67
130
  def add_another_range(start, finish)
68
131
  (start..finish).each { |vlan| @another_in_ranges << vlan }
69
132
  end
70
133
 
71
134
  # Повертаємо масив "інших" VLAN-ів
135
+ # @return [Array<Integer>] Sorted array of unique "another" VLAN IDs
72
136
  def another_in_ranges
73
137
  @another_in_ranges.uniq.sort
74
138
  end
@@ -84,6 +148,9 @@ module FreeRange
84
148
 
85
149
  # Клас для виводу даних
86
150
  class Print
151
+ # Виводить хеш діапазонів VLAN у порядку зростання
152
+ # @param ranges [Ranges] Object containing VLAN ranges
153
+ # @return [void]
87
154
  def self.ranged(ranges)
88
155
  puts "\nСформований хеш діапазонів (в порядку зростання):"
89
156
  if ranges.ranges.empty?
@@ -95,6 +162,9 @@ module FreeRange
95
162
  end
96
163
  end
97
164
 
165
+ # Виводить зайняті VLAN-и в межах діапазонів у порядку зростання
166
+ # @param vlans [Vlans] Object containing VLANs
167
+ # @return [void]
98
168
  def self.vlans(vlans)
99
169
  puts "\nЗайняті VLAN-и в межах діапазонів (в порядку зростання):"
100
170
  if vlans.vlans.empty?
@@ -104,6 +174,9 @@ module FreeRange
104
174
  end
105
175
  end
106
176
 
177
+ # Виводить діапазони VLAN-ів у порядку зростання
178
+ # @param vlans [Vlans] Object containing VLANs
179
+ # @return [void]
107
180
  def self.vlan_ranges(vlans)
108
181
  puts "\nДіапазони VLAN-ів (в порядку зростання):"
109
182
  vlan_ranges = vlans.vlan_ranges
@@ -116,6 +189,13 @@ module FreeRange
116
189
  end
117
190
  end
118
191
 
192
+ # Виводить комбіновані діапазони VLAN зі статусами
193
+ # @param ranges [Ranges] Object containing VLAN ranges
194
+ # @param vlans [Vlans] Object containing VLANs
195
+ # @param use_color [Boolean] Enable colored output
196
+ # @param target [String] Target device hostname
197
+ # @param interface [String, nil] Interface name or nil
198
+ # @return [void]
119
199
  def self.combined_ranges(ranges, vlans, use_color, target, interface = nil)
120
200
  if ranges.ranges.empty?
121
201
  puts "Не знайдено діапазонів."
@@ -142,6 +222,13 @@ module FreeRange
142
222
  puts result.join(',')
143
223
  end
144
224
 
225
+ # Виводить таблицю розподілу VLAN
226
+ # @param ranges [Ranges] Object containing VLAN ranges
227
+ # @param vlans [Vlans] Object containing VLANs
228
+ # @param use_color [Boolean] Enable colored output
229
+ # @param target [String] Target device hostname
230
+ # @param interface [String, nil] Interface name or nil
231
+ # @return [void]
145
232
  def self.table(ranges, vlans, use_color, target, interface = nil)
146
233
  puts "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}"
147
234
  all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
@@ -191,6 +278,13 @@ module FreeRange
191
278
  puts summary_text
192
279
  end
193
280
 
281
+ # Зберігає таблицю розподілу VLAN як PNG-зображення
282
+ # @param ranges [Ranges] Object containing VLAN ranges
283
+ # @param vlans [Vlans] Object containing VLANs
284
+ # @param path [String] Directory path to save the PNG
285
+ # @param target [String] Target device hostname
286
+ # @param interface [String, nil] Interface name or nil
287
+ # @return [void]
194
288
  def self.table_png(ranges, vlans, path, target, interface = nil)
195
289
  all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
196
290
  cell_width = 12
@@ -307,6 +401,10 @@ module FreeRange
307
401
 
308
402
  private
309
403
 
404
+ # Будує хеш статусів VLAN і підраховує кількість кожного статусу
405
+ # @param ranges [Ranges] Object containing VLAN ranges
406
+ # @param vlans [Vlans] Object containing VLANs
407
+ # @return [Array<Hash, Hash>] Hash of VLAN statuses and status counts
310
408
  def self.build_vlan_statuses(ranges, vlans)
311
409
  all_vlans = {}
312
410
  ranges.ranges.each do |start, finish|
@@ -321,6 +419,12 @@ module FreeRange
321
419
  [all_vlans, status_counts]
322
420
  end
323
421
 
422
+ # Форматує діапазон VLAN зі статусом для виводу
423
+ # @param start [Integer] Start of VLAN range
424
+ # @param finish [Integer] End of VLAN range
425
+ # @param status [String] VLAN status ('f', 'b', 'e', 'c', 'a', 'u')
426
+ # @param use_color [Boolean] Enable colored output
427
+ # @return [String] Formatted range string
324
428
  def self.format_range(start, finish, status, use_color)
325
429
  range_text = start == finish ? "#{start}" : "#{start}-#{finish}"
326
430
  range_text_with_status = "#{range_text}(#{status})"
@@ -339,6 +443,10 @@ module FreeRange
339
443
  end
340
444
  end
341
445
 
446
+ # Форматує символ для таблиці VLAN
447
+ # @param status [String] VLAN status ('f', 'b', 'e', 'c', 'a', 'u', or ' ')
448
+ # @param use_color [Boolean] Enable colored output
449
+ # @return [String] Formatted character for table display
342
450
  def self.format_table_char(status, use_color)
343
451
  if use_color
344
452
  case status
@@ -357,12 +465,14 @@ module FreeRange
357
465
  end
358
466
 
359
467
  # Метод для заповнення Ranges для одного інтерфейсу
360
- def self.process_interface(ssh_command, interface, ranges)
361
- command_ranges = interface ? "show configuration interfaces #{interface} | no-more | display set | match dynamic-profile | match \"ranges ([0-9]+(-[0-9]+)?)\"" : 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
362
- command_demux = interface ? "show configuration interfaces #{interface} | display set | match unnumbered-address" : 'show configuration interfaces | display set | match unnumbered-address'
363
- command_another = interface ? "show configuration interfaces #{interface} | display set | match vlan-id" : 'show configuration interfaces | display set | match vlan-id'
364
-
365
- full_cmd = "#{ssh_command} '#{command_ranges}'"
468
+ # @param config [Config] Configuration object with commands
469
+ # @param interface [String, nil] Interface name or nil for all interfaces
470
+ # @param ranges [Ranges] Object to store VLAN ranges
471
+ # @param debug [Boolean] Enable debug output
472
+ # @return [void]
473
+ def self.process_interface(config, interface, ranges, debug = false)
474
+ full_cmd = "#{config.ssh_command} '#{config.command_ranges(interface)}'"
475
+ puts "[DEBUG] Executing command: #{full_cmd}" if debug
366
476
  result = `#{full_cmd}`.strip
367
477
  unless result.empty?
368
478
  result.each_line do |line|
@@ -374,7 +484,8 @@ module FreeRange
374
484
  end
375
485
  end
376
486
 
377
- full_cmd = "#{ssh_command} '#{command_demux}'"
487
+ full_cmd = "#{config.ssh_command} '#{config.command_demux(interface)}'"
488
+ puts "[DEBUG] Executing command: #{full_cmd}" if debug
378
489
  result = `#{full_cmd}`.strip
379
490
  unless result.empty?
380
491
  result.each_line do |line|
@@ -388,7 +499,8 @@ module FreeRange
388
499
  end
389
500
  end
390
501
 
391
- full_cmd = "#{ssh_command} '#{command_another}'"
502
+ full_cmd = "#{config.ssh_command} '#{config.command_another(interface)}'"
503
+ puts "[DEBUG] Executing command: #{full_cmd}" if debug
392
504
  result = `#{full_cmd}`.strip
393
505
  unless result.empty?
394
506
  result.each_line do |line|
@@ -404,10 +516,16 @@ module FreeRange
404
516
  end
405
517
 
406
518
  # Основна логіка виконання
519
+ # @return [void]
407
520
  def self.run
408
521
  options = {}
409
522
  OptionParser.new do |opts|
410
- opts.banner = "Використання: free-range <IP-адреса або hostname> [опції]"
523
+ opts.banner = <<~BANNER
524
+ Використання: free-range <IP-адреса або hostname> [опції]
525
+
526
+ Аналізує розподіл VLAN на мережевих пристроях, генеруючи таблиці або PNG-зображення.
527
+ BANNER
528
+ opts.on("-h", "--help", "Показати цю довідку") { puts opts; exit 0 }
411
529
  opts.on("-u", "--username USERNAME", "Ім'я користувача для SSH") { |u| options[:username] = u }
412
530
  opts.on("-p", "--password PASSWORD", "Пароль для SSH") { |p| options[:password] = p }
413
531
  opts.on("-n", "--no-color", "Вимкнути кольоровий вивід") { options[:no_color] = true }
@@ -415,50 +533,72 @@ module FreeRange
415
533
  opts.on("-t", "--table", "Вивести діаграму розподілу VLAN-ів") { options[:table] = true }
416
534
  opts.on("-g", "--table-png PATH", "Зберегти діаграму розподілу VLAN-ів як PNG") { |path| options[:table_png] = path }
417
535
  opts.on("-i", "--interface INTERFACE", "Назва інтерфейсу або 'all'") { |i| options[:interface] = i }
536
+ opts.on("-c", "--config CONFIG_FILE", "Шлях до конфігураційного файлу") { |c| options[:config_file] = c }
418
537
  end.parse!
419
538
 
420
539
  if ARGV.empty?
421
540
  puts "Помилка: потрібно вказати IP-адресу або hostname роутера."
422
- puts "Використання: free-range <IP-адреса або hostname> [-u|--username USERNAME] [-p|--password PASSWORD] [-n|--no-color] [-d|--debug] [-t|--table] [--table-png PATH] [-i|--interface INTERFACE]"
423
- exit 1
424
- end
425
-
426
- username = options[:username] || ENV['WHOAMI']
427
- password = options[:password] || ENV['WHATISMYPASSWD']
428
- if username.nil? || password.nil?
429
- puts "Помилка: необхідно вказати ім'я користувача та пароль."
430
- puts "Використовуйте опції -u|--username і -p|--password або змінні оточення WHOAMI і WHATISMYPASSWD."
541
+ puts "Використовуйте: free-range --help для довідки."
431
542
  exit 1
432
543
  end
433
544
 
545
+ # Визначаємо змінні з опцій
434
546
  use_color = !options[:no_color] && ENV['TERM'] && ENV['TERM'] != 'dumb'
435
547
  debug = options[:debug]
436
548
  table_mode = options[:table]
437
549
  table_png_mode = options[:table_png]
438
550
  interface = options[:interface]
551
+ config_file = options[:config_file]
552
+
553
+ # Ініціалізуємо config з порожнім login
554
+ config = Config.new({ target: ARGV[0], username: nil, password: nil })
555
+
556
+ # Завантажуємо конфігураційний файл, якщо він вказаний
557
+ if config_file
558
+ begin
559
+ # Виконуємо конфігураційний файл у контексті існуючого об’єкта config
560
+ config.instance_eval(File.read(config_file), config_file)
561
+ rescue LoadError, Errno::ENOENT
562
+ puts "Помилка: неможливо завантажити конфігураційний файл '#{config_file}'."
563
+ exit 1
564
+ rescue ArgumentError => e
565
+ puts "Помилка в конфігураційному файлі '#{config_file}': #{e.message}"
566
+ exit 1
567
+ rescue StandardError => e
568
+ puts "Помилка в конфігураційному файлі '#{config_file}': #{e.message}"
569
+ exit 1
570
+ end
571
+ end
572
+
573
+ # Визначаємо username і password з пріоритетом: аргументи > config > ENV
574
+ username = options[:username] || config.username || ENV['WHOAMI']
575
+ password = options[:password] || config.password || ENV['WHATISMYPASSWD']
439
576
 
440
- # # Перевірка формату інтерфейсу
441
- # if interface && interface != "all" && interface !~ /^[a-z]+-\d+\/\d+\/\d+$/
442
- # puts "Помилка: некоректна назва інтерфейсу. Використовуйте формат 'xe-0/0/2' або 'all'."
443
- # exit 1
444
- # end
577
+ if username.nil? || password.nil?
578
+ puts "Помилка: необхідно вказати ім'я користувача та пароль."
579
+ puts "Використовуйте опції -u/--username і -p/--password, конфігураційний файл або змінні оточення WHOAMI і WHATISMYPASSWD."
580
+ exit 1
581
+ end
445
582
 
446
583
  login = { target: ARGV[0], username: username, password: password }
447
584
  target = ARGV[0].split('.')[0]
448
585
  puts "Connecting to device: #{login[:target]}"
449
586
 
450
- ssh_command = "sshpass -p \"#{login[:password]}\" ssh -C -x -4 -o StrictHostKeyChecking=no #{login[:username]}@#{login[:target]}"
451
- subscribers_command = "ssh -C -x roffice /usr/local/share/noc/bin/radius-subscribers"
587
+ # Оновлюємо config з актуальними login даними
588
+ config = Config.new(login) { |c|
589
+ c.username = config.username if config.username
590
+ c.password = config.password if config.password
591
+ }
452
592
 
453
- subscribers_result = `#{subscribers_command}`.strip
593
+ subscribers_result = `#{config.subscribers_command}`.strip
454
594
  if subscribers_result.empty?
455
595
  puts "Помилка: результат subscribers_command порожній. Перевір шлях або доступ."
456
596
  exit 1
457
597
  end
458
598
 
459
599
  if interface == "all"
460
- command_interfaces = 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
461
- full_cmd = "#{ssh_command} '#{command_interfaces}'"
600
+ full_cmd = "#{config.ssh_command} '#{config.command_interfaces}'"
601
+ puts "[DEBUG] Executing command: #{full_cmd}" if debug
462
602
  result = `#{full_cmd}`.strip
463
603
  if result.empty?
464
604
  puts "Помилка: результат команди порожній. Перевір підключення або команду."
@@ -481,7 +621,7 @@ module FreeRange
481
621
  end
482
622
  end
483
623
 
484
- process_interface(ssh_command, intf, ranges)
624
+ process_interface(config, intf, ranges, debug)
485
625
  if debug
486
626
  puts "\nІнтерфейс: #{intf}"
487
627
  Print.ranged(ranges)
@@ -511,7 +651,7 @@ module FreeRange
511
651
  end
512
652
  end
513
653
 
514
- process_interface(ssh_command, interface, ranges)
654
+ process_interface(config, interface, ranges, debug)
515
655
  if debug
516
656
  puts "\nІнтерфейс: #{interface}" if interface
517
657
  Print.ranged(ranges)