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 +4 -0
- data/Rakefile +2 -0
- data/bin/mtik_ppp_active_2_directory +91 -0
- data/lib/mtik_ppp_active_2_directory.rb +66 -0
- data/lib/mtik_ppp_active_2_directory/directory.rb +76 -0
- data/lib/mtik_ppp_active_2_directory/log.rb +18 -0
- data/lib/mtik_ppp_active_2_directory/mikrotik.rb +121 -0
- data/lib/mtik_ppp_active_2_directory/version.rb +3 -0
- data/mtik_ppp_active_2_directory.gemspec +22 -0
- metadata +104 -0
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -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,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: []
|