omnibot 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ OmniBot
2
+ ===============
3
+
4
+ Simple XMPP bot for server monitoring.
5
+ Works with AMQP for sending messages at server side.
6
+ Sends notifications to a user via XMPP.
7
+
8
+ Dependencies
9
+ ------------
10
+
11
+ * RabbitMQ or any other AMQP-compatible server
12
+ * amqp
13
+ * xmpp4r
14
+ * eventmachine
15
+
16
+ Installation
17
+ ------------
18
+
19
+ Configure omnibot configuration from examples/config.yaml to ~/.omnibot.yaml and adjust it.
20
+ Then execute command:
21
+
22
+ omnibot
23
+
24
+ Send messages to omnibot by AMQP by running:
25
+
26
+ omnisend 'Hello World!'
27
+
28
+ Support
29
+ -------
30
+
31
+ Tested on Mac OS X 10.6 with Ruby 1.8
@@ -0,0 +1,36 @@
1
+ begin
2
+ require "bundler"
3
+ Bundler.setup
4
+ rescue LoadError
5
+ $stderr.puts "You need to have Bundler installed to be able build this gem."
6
+ end
7
+
8
+ gemspec = eval(File.read(Dir["*.gemspec"].first))
9
+
10
+
11
+ desc "Validate the gemspec"
12
+ task :gemspec do
13
+ gemspec.validate
14
+ end
15
+
16
+ desc "Build gem locally"
17
+ task :build => :gemspec do
18
+ system "gem build #{gemspec.name}.gemspec"
19
+ FileUtils.mkdir_p "pkg"
20
+ FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", "pkg"
21
+ end
22
+
23
+ desc "Install gem locally"
24
+ task :install => :build do
25
+ system "gem install pkg/#{gemspec.name}-#{gemspec.version}"
26
+ end
27
+
28
+ desc "Push gem"
29
+ task :push => :build do
30
+ system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
31
+ end
32
+
33
+ desc "Clean automatically generated files"
34
+ task :clean do
35
+ FileUtils.rm_rf "pkg"
36
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'omnibot'
4
+
5
+ OmniBot::Launcher.new.start(ARGV)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'omnibot'
4
+
5
+ OmniBot::OmniSend.new.start(ARGV)
@@ -0,0 +1,7 @@
1
+ config:
2
+ omnibotuser: bot@example.com
3
+ omnibotpass: secret
4
+ notifyjid: owner@example.com
5
+ notifyresource: Adium.*
6
+ periodiccommands: [ 'vnstat' ]
7
+ logpath: '/tmp/omnibot.log'
@@ -0,0 +1,13 @@
1
+ require 'yaml'
2
+ require 'logger'
3
+
4
+ require 'amqp'
5
+ require 'mq'
6
+ require 'eventmachine'
7
+ require 'xmpp4r'
8
+ require 'xmpp4r/client'
9
+ require 'xmpp4r/roster'
10
+
11
+ %w[ helpers jabberbot amqpconsumer omnisend launcher periodiccommand ].each do |file|
12
+ require "omnibot/#{file}.rb"
13
+ end
@@ -0,0 +1,74 @@
1
+ module OmniBot
2
+
3
+ # AMQP consumer class
4
+ class AMQPConsumer
5
+
6
+ def send_message message
7
+ begin
8
+ @omnibot.add_message [Time.now, message]
9
+ rescue Object => e
10
+ OmniLog::error "Sending message error: #{e.message}\ntrace:\n#{Helpers::backtrace e}\nIgnoring..."
11
+ end
12
+ end
13
+
14
+ def initialize config
15
+ @config = config
16
+ end
17
+
18
+ def amqp_loop
19
+ # setup amqp
20
+ mq = MQ.new
21
+ exchange = mq.direct(Helpers::amqp_exchange_name)
22
+ queue = mq.queue("omnibot-consumerqueue", :exclusive => true)
23
+ queue.bind(exchange)
24
+
25
+ begin
26
+ OmniLog::info "Setup omnibot..."
27
+ @omnibot = JabberBot.new(Jabber::JID::new(@config['omnibotuser']), @config['omnibotpass'])
28
+ @omnibot.timer_provider = EM
29
+ @omnibot.set_subscriber Jabber::JID::new(@config['notifyjid']), @config['notifyresource']
30
+ @omnibot.connect
31
+
32
+ pause = 0
33
+ [@config['periodiccommands']].flatten.each do |command|
34
+ OmniLog::info "Setup command #{command}..."
35
+ periodic_command = PeriodicCommand.new command, pause
36
+ periodic_command.timer_provider = EM
37
+ periodic_command.set_jabber_messenger { |message| send_message message }
38
+ periodic_command.start
39
+ pause += 20
40
+ end
41
+
42
+ rescue Object => e
43
+ OmniLog::error "Sending message error: #{e.message}\ntrace:\n#{Helpers::backtrace e}\nExiting..."
44
+ AMQP.stop{ EM.stop }
45
+ end
46
+
47
+ OmniLog::info "==== AMQP is ready ===="
48
+
49
+ queue.subscribe do |msg|
50
+ message = Marshal.load msg
51
+ send_message message
52
+ end
53
+ end
54
+
55
+ # Main AMQP loop
56
+ def start
57
+
58
+ # exit hook
59
+ Signal.trap('INT') do
60
+ OmniLog::info "It's a trap, should exit..."
61
+ AMQP.stop{ EM.stop }
62
+ end
63
+
64
+ AMQP.start do
65
+ amqp_loop
66
+ end
67
+
68
+ OmniLog::info "Exited"
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
@@ -0,0 +1,65 @@
1
+ module OmniBot
2
+
3
+ class OmniLog
4
+
5
+ def self.init_log
6
+ logger = Logger.new('omnibot.log')
7
+ logger.level = Logger::DEBUG
8
+ logger
9
+ end
10
+
11
+ def self.log=(value)
12
+ @logger = value
13
+ end
14
+
15
+ def self.log
16
+ @logger
17
+ end
18
+
19
+ def self.debug(progname = nil, &block); @logger.debug(progname, &block); end
20
+ def self.info(progname = nil, &block); @logger.info(progname, &block); end
21
+ def self.warn(progname = nil, &block); @logger.warn(progname, &block); end
22
+ def self.error(progname = nil, &block); @logger.error(progname, &block); end
23
+ def self.fatal(progname = nil, &block); @logger.fatal(progname, &block); end
24
+ end
25
+
26
+ # Helper class for counting reconnect attempts
27
+ class AttemptCounter
28
+ def report
29
+ OmniLog::debug "AttemptCounter: try #{@counter} of #{@max_attempts}"
30
+ end
31
+ public
32
+ def initialize max_attempts
33
+ @counter = 0
34
+ @max_attempts = max_attempts
35
+ OmniLog::debug "AttemptCounter inited"
36
+ end
37
+
38
+ def out_of_attempts?
39
+ @counter >= @max_attempts
40
+ end
41
+
42
+ def increase
43
+ @counter += 1
44
+ report
45
+ end
46
+ end
47
+
48
+ class Helpers
49
+ def self.backtrace e
50
+ e.respond_to?(:backtrace) && e.backtrace ? e.backtrace.join("\n\t") : ""
51
+ end
52
+
53
+ def self.same_day? t1, t2
54
+ t1.year == t2.year && t1.month == t2.month && t1.day == t2.day
55
+ end
56
+
57
+ def self.amqp_exchange_name
58
+ 'omnibot-exchange'
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+
65
+
@@ -0,0 +1,186 @@
1
+ module OmniBot
2
+
3
+ # Jabber bot with reconnection and dnd-care logic
4
+ class JabberBot
5
+
6
+ def dump_presence p
7
+ p ? "Presence status=#{p.status} type=#{p.type} show=#{p.show} from=#{p.from} to=#{p.to} xml=(#{p.to_s})" : "nil"
8
+ end
9
+
10
+ def on_message_handler m
11
+ OmniLog::debug "Got jabber message from #{m.from}:\n#{m.body}\n."
12
+ end
13
+
14
+ def is_needed_user? jid
15
+ jid.strip == @subscriber && @subscriber_resource.match((jid.resource or ''))
16
+ end
17
+
18
+ def on_presence_callback old_presence, new_presence
19
+ OmniLog::debug "Presence changed:\n...old #{dump_presence old_presence}\n...new #{dump_presence new_presence}"
20
+ if is_needed_user? old_presence.from
21
+ @subscriber_online = check_presence? old_presence
22
+ OmniLog::debug "Subscriber #{@subscriber} is #{@subscriber_online ? "ready" : "not ready"}"
23
+ pump_messages if @subscriber_online
24
+ end
25
+ end
26
+
27
+ def on_subscripton_request_callback item, pres
28
+ OmniLog::debug "Subscription request item=#{item} pres=#{dump_presence pres}"
29
+ end
30
+
31
+ def on_exception_handler e, stream, sym_where
32
+ OmniLog::error "Jabber exception happens at symbol \"#{sym_where}\": #{e}\nbacktrace\n#{Helpers::backtrace e}"
33
+ OmniLog::debug "stream is #{stream} vs client #{@client}"
34
+ on_generic_exception_handler e
35
+ end
36
+
37
+ def safe_reconnect
38
+ begin
39
+ reconnect
40
+ rescue Jabber::ClientAuthenticationFailure => e
41
+ OmniLog::error "Authentification error: #{e.class}: #{e}"
42
+ raise
43
+ rescue Exception => e
44
+ OmniLog::error "Reconnect hard error: #{e.class}: #{e}"
45
+ on_generic_exception_handler e
46
+ end
47
+ end
48
+
49
+ def on_generic_exception_handler e
50
+ if e && (e.kind_of?(Jabber::ServerDisconnected) || e.class.to_s =~ /^Errno::.+/)
51
+ OmniLog::error "No timer provider assigned" unless @timer_provider
52
+ # attempt counter is set when it's needed to connect
53
+ unless @ignore_reconnect
54
+ @timer_provider.add_timer(@reconnect_pause) { try_reconnect }
55
+ end
56
+ end
57
+ end
58
+
59
+ def reconnect
60
+ OmniLog::debug 'Going to reconnect'
61
+ @client.connect
62
+ @client.auth(@password)
63
+ @client.send(Jabber::Presence.new.set_type(:available))
64
+ end
65
+
66
+ def try_reconnect
67
+ return if @client.is_connected?
68
+
69
+ OmniLog::debug 'Called try_reconnect'
70
+
71
+ @attempt_counter = AttemptCounter.new(5) unless @attempt_counter
72
+ @attempt_counter.increase
73
+
74
+ if @attempt_counter.out_of_attempts?
75
+ OmniLog::warn "Can't reconect too often, sleep for #{@reconnect_long_pause/60} minutes..."
76
+ @attempt_counter = nil
77
+ @ignore_reconnect = true
78
+ @timer_provider.add_timer(@reconnect_long_pause) {
79
+ @ignore_reconnect = false
80
+ try_reconnect
81
+ }
82
+ return
83
+ end
84
+
85
+ safe_reconnect
86
+
87
+ if @client.is_connected?
88
+ @attempt_counter = nil
89
+ @roster = Jabber::Roster::Helper.new(@client)
90
+ @roster.add_subscription_request_callback { |item, pres| on_subscripton_request_callback item, pres }
91
+ end
92
+
93
+ OmniLog::debug "Client #{@client.is_connected? ? 'is' : 'isn\'t'} connected"
94
+ end
95
+
96
+ def check_presence? presence
97
+ raise 'No subscriber' unless @subscriber
98
+
99
+ OmniLog::debug "Subscriber #{@subscriber} is #{presence.show ? presence.show : 'online'}"
100
+ presence.show == nil || presence.show == :chat
101
+ end
102
+
103
+ def say_when_human orig, now
104
+ if Helpers::same_day? now, orig
105
+ amount = now - orig
106
+ if amount < 60
107
+ return "just now"
108
+ elsif amount < 60*60
109
+ return "less than a hour ago"
110
+ elsif amount < 60*60*6
111
+ return amount.div(60).to_s + " hours ago"
112
+ end
113
+ end
114
+ return orig.to_s
115
+ end
116
+
117
+
118
+ def pump_messages
119
+ while msg = @messages.shift
120
+ send msg
121
+ end
122
+ end
123
+
124
+ public
125
+
126
+ attr_writer :timer_provider
127
+
128
+ def initialize jid, password
129
+ @client = Jabber::Client::new(jid)
130
+ @password = password
131
+ raise 'No jid set' if jid.empty?
132
+ raise 'No password set' unless password
133
+
134
+ @ignore_reconnect = false
135
+ @reconnect_pause = 10
136
+ @reconnect_long_pause = 60*15
137
+
138
+ @messages = []
139
+ @subscriber_online = false
140
+
141
+ @client.on_exception { |e, stream, sym_where| on_exception_handler(e, stream, sym_where) }
142
+ @client.add_message_callback { |m| on_message_handler m }
143
+ @client.add_presence_callback { |from, to| on_presence_callback from, to }
144
+ end
145
+
146
+ def connect
147
+ try_reconnect
148
+ end
149
+
150
+ def disconnect
151
+ @client.close
152
+ end
153
+
154
+ def set_subscriber jid, resource=nil
155
+ @subscriber = jid
156
+ if resource == nil || resource == ''
157
+ @subscriber_resource = /.*/
158
+ else
159
+ @subscriber_resource = Regexp.new(resource)
160
+ end
161
+ end
162
+
163
+ def add_message message
164
+ OmniLog::debug "Register a message, " + (@subscriber_online ? "should send immediately" : "will send later")
165
+ @messages << message
166
+ pump_messages if @subscriber_online
167
+ end
168
+
169
+ def send message
170
+ raise 'Not connected' unless @client.is_connected?
171
+
172
+ OmniLog::info "Sending a message..."
173
+ orig = message[0]
174
+ content = message[1]
175
+
176
+ body = "Omnibot reported " + say_when_human(orig, Time.now) + ":\n" + content.to_s
177
+ OmniLog::debug body
178
+
179
+ msg = Jabber::Message::new(@subscriber, body)
180
+ msg.type = :chat
181
+ @client.send(msg)
182
+ end
183
+ end
184
+
185
+ end
186
+
@@ -0,0 +1,37 @@
1
+ module OmniBot
2
+
3
+ # entry point
4
+ class Launcher
5
+ def get_config_path args
6
+ return args[0] if args[0] && File.file?(args[0])
7
+ path = File.join(ENV['HOME'],'.omnibot.yaml')
8
+ return path if ENV['HOME'] && File.file?(path)
9
+ raise 'No config file found, checked command line and ~/.omnibot.yaml'
10
+ end
11
+
12
+ def get_log_path config
13
+ return config['logpath'] unless (config['logpath'] or '').empty?
14
+ return '/var/log/omnibot.log' if File.directory? '/var/log/'
15
+ return 'omnibot.log'
16
+ end
17
+
18
+ def start args
19
+ config_path = get_config_path args
20
+ puts "Using config at #{config_path}"
21
+ config = YAML.load_file(config_path)["config"]
22
+
23
+ log_path = get_log_path config
24
+ puts "Using log at #{log_path}"
25
+ OmniLog::log = Logger.new(log_path)
26
+ OmniLog::log.level = Logger::DEBUG
27
+
28
+ consumer = AMQPConsumer.new config
29
+ consumer.start
30
+
31
+ OmniLog::log.close
32
+ end
33
+ end
34
+ end
35
+
36
+
37
+
@@ -0,0 +1,23 @@
1
+ module OmniBot
2
+
3
+ class OmniSend
4
+
5
+ def start args
6
+ return 1 if args.empty?
7
+ message = args.join(' ')
8
+ puts "Sending message #{message}"
9
+ data = Marshal.dump(message)
10
+
11
+ Signal.trap('INT') { AMQP.stop{ EM.stop } }
12
+
13
+ AMQP.start do
14
+ mq = MQ.new
15
+ exchange = mq.direct(Helpers::amqp_exchange_name)
16
+ exchange.publish(data)
17
+ puts 'sent'
18
+ AMQP.stop{ EM.stop }
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,48 @@
1
+ module OmniBot
2
+
3
+ # Send to jabber user result of a daily command
4
+ class PeriodicCommand
5
+
6
+ def on_first_timer
7
+ OmniLog::debug "Okay, it's near of midnight"
8
+ on_periodic_timer
9
+ @timer_provider.add_periodic_timer(24*3600) { on_periodic_timer }
10
+ end
11
+
12
+ def on_periodic_timer
13
+ OmniLog::info "Reporting command #{@command}"
14
+ body = `#{@command}`
15
+ raise 'Error launching command ' if $? != 0
16
+ message_body = "Results of daily executed command #{@command}:\n" + body
17
+ @jabber_messenger.call message_body
18
+ end
19
+
20
+ public
21
+ attr_writer :timer_provider
22
+
23
+ def initialize command, pause
24
+ @command = command
25
+ @pause = pause
26
+
27
+ raise 'Wrong command' if (command == nil or command == '')
28
+ end
29
+
30
+ def start
31
+ `command -v #{@command}`
32
+ if $? != 0
33
+ OmniLog::warn "Command #{@command} is not available"
34
+ else
35
+ now = Time.now
36
+ next_report_time = Time.local(now.year, now.month, now.day+1, 1, 0, 0)
37
+ next_report_time = next_report_time + @pause
38
+ @timer_provider.add_timer(next_report_time - now) { on_first_timer }
39
+ end
40
+ end
41
+
42
+ def set_jabber_messenger &block
43
+ @jabber_messenger = block
44
+ end
45
+ end
46
+
47
+ end
48
+
@@ -0,0 +1,3 @@
1
+ module OmniBot
2
+ VERSION = '0.0.2'
3
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omnibot
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - theirix
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-10 00:00:00 +03:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ hash: 3
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ prerelease: false
32
+ version_requirements: *id001
33
+ type: :runtime
34
+ name: xmpp4r
35
+ - !ruby/object:Gem::Dependency
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ hash: 3
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ prerelease: false
46
+ version_requirements: *id002
47
+ type: :runtime
48
+ name: eventmachine
49
+ - !ruby/object:Gem::Dependency
50
+ requirement: &id003 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ hash: 3
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ prerelease: false
60
+ version_requirements: *id003
61
+ type: :runtime
62
+ name: amqp
63
+ description: Works with AMQP for sending messages at server side.Sends notifications to an user via XMPP.Can monitor system by performing daily commands.
64
+ email: theirix@gmail.com
65
+ executables:
66
+ - omnibot
67
+ - omnisend
68
+ extensions: []
69
+
70
+ extra_rdoc_files: []
71
+
72
+ files:
73
+ - examples/config.yaml
74
+ - lib/omnibot/amqpconsumer.rb
75
+ - lib/omnibot/helpers.rb
76
+ - lib/omnibot/jabberbot.rb
77
+ - lib/omnibot/launcher.rb
78
+ - lib/omnibot/omnisend.rb
79
+ - lib/omnibot/periodiccommand.rb
80
+ - lib/omnibot/version.rb
81
+ - lib/omnibot.rb
82
+ - Rakefile
83
+ - README.md
84
+ - bin/omnibot
85
+ - bin/omnisend
86
+ has_rdoc: true
87
+ homepage: http://github.com/theirix/omnibot
88
+ licenses: []
89
+
90
+ post_install_message:
91
+ rdoc_options: []
92
+
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 31
101
+ segments:
102
+ - 1
103
+ - 8
104
+ version: "1.8"
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ hash: 3
111
+ segments:
112
+ - 0
113
+ version: "0"
114
+ requirements:
115
+ - AMQP-compatible server (for example, RabbitMQ)
116
+ rubyforge_project: nowarning
117
+ rubygems_version: 1.4.2
118
+ signing_key:
119
+ specification_version: 3
120
+ summary: Simple XMPP bot for server monitoring
121
+ test_files: []
122
+