mtik_ppp_active_2_directory 1.0.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/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mtik_ppp_active_2_directory.gemspec
4
+ gemspec
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+
@@ -0,0 +1,91 @@
1
+ # coding: utf-8
2
+ require 'optparse'
3
+ require 'syslog'
4
+ require 'mtik_ppp_active_2_directory'
5
+
6
+ class Args
7
+ Error = Class.new(StandardError)
8
+
9
+ attr_reader :verbose, :retry_timeout
10
+
11
+ def initialize
12
+ @verbose = STDOUT.tty?
13
+ @retry_timeout = 15 # seconds
14
+ end
15
+
16
+ def self.parse
17
+ self.new.tap { |args| args.parse }
18
+ end
19
+
20
+ def parse
21
+ opts = OptionParser.new
22
+ opts.banner = 'Usage: <host> <username> <password> <dir> [options]'
23
+
24
+ opts.separator ''
25
+ opts.separator 'Options:'
26
+ opts.on('-v', '--[no-]verbose', 'Print debug messages to stdout',
27
+ '(default: true if stdout is a TTY)') { |v| @verbose = v }
28
+ opts.on('--retry-timeout SECONDS', Integer, 'Wait before restarting after an error',
29
+ "(default: #{@retry_timeout})") { |i| @retry_timeout = i }
30
+
31
+ opts.separator ''
32
+ opts.separator 'IP addresses assigned to the remote end of PPP connections will be'
33
+ opts.separator 'created in <dir> as a symbolic link pointing to the interface name.'
34
+ opts.separator ''
35
+
36
+ opts.parse!
37
+ ARGV.size == 4 || raise(Error, "need 4 arguments, but #{ARGV.size} provided")
38
+
39
+ rescue Error, OptionParser::ParseError => err
40
+ STDERR << opts << 'Error: ' << err << "\n"
41
+ exit(2)
42
+ end
43
+
44
+ def directory
45
+ ARGV[3]
46
+ end
47
+
48
+ def mikrotik
49
+ Hash[[:host, :user, :pass].zip(ARGV[0, 3])]
50
+ end
51
+ end
52
+
53
+ class Main
54
+ def initialize
55
+ args
56
+ sync
57
+ end
58
+
59
+ def args
60
+ @args ||= Args.parse.tap do |args|
61
+ Syslog.open($PROGRAM_NAME, Syslog::LOG_PID, Syslog::LOG_DAEMON)
62
+ args.verbose && MtikPppActive2Directory::Log.output(&method(:info))
63
+ end
64
+ end
65
+
66
+ def sync
67
+ MtikPppActive2Directory.sync(args.mikrotik, args.directory)
68
+ rescue => err
69
+ err("Error: #{err}")
70
+ err.backtrace.each { |line| err(" #{line}") }
71
+ sleep(args.retry_timeout)
72
+ retry
73
+ end
74
+
75
+ # Send +message+ to syslog and STDERR.
76
+ # @param [String] message
77
+ # @return [void]
78
+ def err(message)
79
+ Syslog.err('%s', message)
80
+ STDERR << message << "\n" if STDERR.tty?
81
+ end
82
+
83
+ # Send +message+ to STDOUT.
84
+ # @param [String] message
85
+ # @return [void]
86
+ def info(message)
87
+ STDOUT << message << "\n"
88
+ end
89
+ end
90
+
91
+ Main.new
@@ -0,0 +1,66 @@
1
+ require 'mtik_ppp_active_2_directory/version'
2
+ require 'mtik_ppp_active_2_directory/log'
3
+ require 'mtik_ppp_active_2_directory/directory'
4
+ require 'mtik_ppp_active_2_directory/mikrotik'
5
+
6
+ module MtikPppActive2Directory
7
+ # Synchronize Mikrotik's PPP active connections with a directory containing symbolic links.
8
+ #
9
+ # @param [Hash] src The object passed to the +mtik+ gem (keys: +host+, +user+, +pass+)
10
+ # @param [String] dst The directory holding the symbolic links
11
+ def self.sync(src, dst)
12
+ Log.info { "Synchronizing router at [#{src[:host]}] with directory [#{dst}]" }
13
+ Sync.new(src, dst).sync
14
+ end
15
+
16
+ class Sync
17
+ def initialize(src, dst)
18
+ @mtik = Mikrotik.new(src)
19
+ @dir = Directory.new(path:dst)
20
+ end
21
+
22
+ def sync
23
+ @mtik.watch { |*args| send(*args) }
24
+ end
25
+
26
+ # @param [Hash] mm Mikrotik Map {IP => user}
27
+ def start(mm)
28
+ dm = Hash[@dir.list.to_a] # Directory Map {IP => user}
29
+ mm.each_pair do |m_ip, m_user|
30
+ if dm.key? m_ip
31
+ if m_user != dm[m_ip]
32
+ update(m_ip, m_user)
33
+ end
34
+ else
35
+ add(m_ip, m_user)
36
+ end
37
+ end
38
+ dm.each_pair do |d_ip, d_user|
39
+ unless mm.key? d_ip
40
+ delete(d_ip, d_user)
41
+ end
42
+ end
43
+ end
44
+
45
+ # @param [String] ip
46
+ # @param [String] user
47
+ def add(ip, user)
48
+ Log.info { "Adding IP [#{ip}] user [#{user}]" }
49
+ @dir[ip] = user
50
+ end
51
+
52
+ # @param [String] ip
53
+ # @param [String] user
54
+ def update(ip, user)
55
+ Log.info { "Updating IP [#{ip}] user [#{user}]" }
56
+ @dir[ip] = user
57
+ end
58
+
59
+ # @param [String] ip
60
+ # @param [String] user
61
+ def delete(ip, user)
62
+ Log.info { "Deleting IP [#{ip}] user [#{user}]" }
63
+ @dir.delete(ip)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,76 @@
1
+ # coding: utf-8
2
+
3
+ module MtikPppActive2Directory
4
+ class Directory
5
+ Error = Class.new(StandardError)
6
+ SyncError = Class.new(Error)
7
+
8
+ # The {#list} method will not enumerate symbolic links that do not match this regular expression.
9
+ # Other methods may raise Error.
10
+ IP_RE = %r{ \A \d{1,3} \. \d{1,3} \. \d{1,3} \. \d{1,3} \z }x
11
+
12
+ # Manage a directory of symbolic links.
13
+ #
14
+ # The IP address is the name of the symbolic link.
15
+ # The user name is what the symbolic link points to.
16
+ #
17
+ # @param [Hash] params
18
+ # @option params [String] :path The path to the directory containing symbolic links
19
+ def initialize(params)
20
+ @path = params[:path]
21
+ end
22
+
23
+ # Return the user name associated with +ip+.
24
+ #
25
+ # @param [String] ip
26
+ #
27
+ # @raise [SyncError] When the +ip+ is not found
28
+ # @return [String] The interface associated with +ip+
29
+ def [](ip)
30
+ (ip =~ IP_RE) || raise(Error, "Invalid IP [#{ip}]")
31
+ file = File.join(@path, ip)
32
+ File.readlink(file)
33
+ rescue Errno::ENOENT
34
+ raise(SyncError, "IP [#{ip}] not found [#{file}]")
35
+ rescue Errno::EINVAL
36
+ raise(Error, "IP [#{ip}] is not a symlink [#{file}]")
37
+ end
38
+
39
+ # Associate +user+ to +ip+.
40
+ #
41
+ # @param [String] ip
42
+ # @param [String] user
43
+ def []=(ip, user)
44
+ (ip =~ IP_RE) || raise(Error, "Invalid IP [#{ip}]")
45
+ file = File.join(@path, ip)
46
+ tmp = "#{file}.tmp"
47
+ File.symlink(user, tmp)
48
+ File.rename(tmp, file)
49
+ end
50
+
51
+ # Delete the +ip+ association.
52
+ #
53
+ # @param [String] ip
54
+ #
55
+ # @raise [SyncError] When the +ip+ is not found
56
+ # @return [void]
57
+ def delete(ip)
58
+ (ip =~ IP_RE) || raise(Error, "Invalid IP [#{ip}]")
59
+ file = File.join(@path, ip)
60
+ File.unlink(file)
61
+ rescue Errno::ENOENT
62
+ raise(SyncError, "IP [#{ip}] not found [#{file}]")
63
+ end
64
+
65
+ # Enumerate all IP associations.
66
+ #
67
+ # @return [Enumerator] Arrays with: IP, user
68
+ def list
69
+ Enumerator.new do |y|
70
+ Dir.foreach(@path) do |de| file = File.basename(de)
71
+ (file =~ IP_RE) && (value = self[file]) && (y << [file, value])
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,18 @@
1
+ # coding: utf-8
2
+
3
+ module MtikPppActive2Directory
4
+ module Log
5
+ module_function
6
+
7
+ # Call #output to replace this method.
8
+ def info
9
+ end
10
+
11
+ # Set the logging method.
12
+ def output(&block)
13
+ define_singleton_method(:info) do |&message|
14
+ block.call(message.call)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,121 @@
1
+ # coding: utf-8
2
+ require 'mtik'
3
+
4
+ module MtikPppActive2Directory
5
+ class Mikrotik
6
+ Error = Class.new(StandardError)
7
+ RouterError = Class.new(Error)
8
+ SyncError = Class.new(Error)
9
+
10
+ def initialize(params)
11
+ @mtik_params = params.select { |k,_| %w(host user pass).include? k.to_s }
12
+ @mtik_params.update(cmd_timeout:86400)
13
+ end
14
+
15
+ # Enumerate the PPP active connections.
16
+ #
17
+ # @return [Enumerator] Arrays with: IP, user
18
+ def list
19
+ Enumerator.new do |y|
20
+ @cache.each_value do |re|
21
+ y << [re['address'], re['name']]
22
+ end
23
+ end
24
+ end
25
+
26
+ # Watch for modifications to the PPP active connections.
27
+ #
28
+ # @yield [method, *data]
29
+ #
30
+ # The first +method+ yielded is :start.
31
+ # +data+ is a Hash of {IP => user}.
32
+ #
33
+ # Afterwards, two +method+s may be yielded: :add and :delete.
34
+ # +data+ is the IP and user, added or deleted.
35
+ def watch(&block)
36
+ @cache = {}
37
+
38
+ # Listen for modifications to the PPP active connections.
39
+ # Since they haven't been fully fetched yet, buffer them for later.
40
+ buffer = []
41
+ request('/ppp/active/listen', '=.proplist=.dead,.id,name,address') do |re|
42
+ buffer ? buffer.push(re) : update(re, &block) # [1]
43
+ end
44
+
45
+ # Fetch all PPP active connections.
46
+ connection.wait_for_request(
47
+ request('/ppp/active/print', '=.proplist=.id,name,address') do |re|
48
+ @cache[re['.id']] = re
49
+ end
50
+ )
51
+
52
+ # Apply modifications that happened during the 'print'.
53
+ # Updates that happened after 'listen' started, but before 'print' completed,
54
+ # may cause #update to raise SyncError. It's probably okay to ignore that.
55
+ buffer.each do |bre|
56
+ update(bre, &block) rescue SyncError
57
+ end
58
+ # With 'print' done, call 'update' directly instead of buffering.
59
+ buffer = nil # See [1]
60
+
61
+ # Start by yielding the entire PPP active connections list.
62
+ # Afterwards, only changes will be yielded.
63
+ yield(:start, Hash[list.to_a])
64
+
65
+ # Give control to the 'mtik' gem.
66
+ connection.wait_all
67
+ raise(Error, "The 'watch' method should never return")
68
+ end
69
+
70
+ private
71
+
72
+ # @param [Hash] re
73
+ # @return [Hash] The same +re+
74
+ def add(re)
75
+ if @cache.key?(re['.id'])
76
+ raise(SyncError, "ID #{re['.id']} already in cache")
77
+ else
78
+ @cache[re['.id']] = re
79
+ end
80
+ end
81
+
82
+ # @param [Hash] re
83
+ # @return [Hash] The deleted +re+
84
+ def delete(re)
85
+ @cache.delete(re['.id']).tap do |deleted_re|
86
+ unless deleted_re
87
+ raise(SyncError, "ID #{re['.id']} not in cache")
88
+ end
89
+ end
90
+ end
91
+
92
+ # @param [Hash] re
93
+ def update(re)
94
+ method = re.key?('.dead') ? :delete : :add
95
+ re = send(method, re)
96
+ yield(method, re['address'], re['name'])
97
+ end
98
+
99
+ # @return [MTik::Connection]
100
+ def connection
101
+ @connection ||= MTik::Connection.new(@mtik_params).tap do
102
+ Log.info { "Connecting to Mikrotik at #{@mtik_params.inspect}" }
103
+ end
104
+ end
105
+
106
+ # @return [MTik::Request]
107
+ def request(*args)
108
+ Log.info { "Mikrotik request: #{args.inspect}" }
109
+ connection.request_each(*args) do |request|
110
+ while (re = request.reply.shift)
111
+ Log.info { "Mikrotik reply: #{re.inspect}" }
112
+ if re.key?('!re') ; yield(re)
113
+ elsif re.key?('!done') ; nil
114
+ elsif re.key?('!trap') ; raise(RouterError, re['message'])
115
+ else raise(Error, "Unrecognized Mikrotik reply: #{re.inspect}")
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,3 @@
1
+ module MtikPppActive2Directory
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mtik_ppp_active_2_directory/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'mtik_ppp_active_2_directory'
8
+ spec.version = MtikPppActive2Directory::VERSION
9
+ spec.authors = ['André Luiz dos Santos']
10
+ spec.email = ['andre.netvision.com.br@gmail.com']
11
+ spec.summary = %q{Synchronize Mikrotik's PPP active users with a directory.}
12
+
13
+ spec.files = `git ls-files -z`.split("\x0")
14
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ['lib']
17
+
18
+ spec.add_development_dependency 'bundler', '~> 1.7'
19
+ spec.add_development_dependency 'rake', '~> 10.0'
20
+
21
+ spec.add_runtime_dependency 'mtik', '~> 4.0'
22
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mtik_ppp_active_2_directory
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - André Luiz dos Santos
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-01-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.7'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.7'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '10.0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '10.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: mtik
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '4.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '4.0'
62
+ description:
63
+ email:
64
+ - andre.netvision.com.br@gmail.com
65
+ executables:
66
+ - mtik_ppp_active_2_directory
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - .gitignore
71
+ - Gemfile
72
+ - Rakefile
73
+ - bin/mtik_ppp_active_2_directory
74
+ - lib/mtik_ppp_active_2_directory.rb
75
+ - lib/mtik_ppp_active_2_directory/directory.rb
76
+ - lib/mtik_ppp_active_2_directory/log.rb
77
+ - lib/mtik_ppp_active_2_directory/mikrotik.rb
78
+ - lib/mtik_ppp_active_2_directory/version.rb
79
+ - mtik_ppp_active_2_directory.gemspec
80
+ homepage:
81
+ licenses: []
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ! '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 1.8.23
101
+ signing_key:
102
+ specification_version: 3
103
+ summary: Synchronize Mikrotik's PPP active users with a directory.
104
+ test_files: []