collins_notify 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +15 -0
- data/README.rdoc +27 -0
- data/Rakefile +74 -0
- data/VERSION +1 -0
- data/bin/collins-notify +8 -0
- data/lib/collins_notify.rb +16 -0
- data/lib/collins_notify/adapter/email.rb +108 -0
- data/lib/collins_notify/adapter/helper/carried-pigeon.rb +213 -0
- data/lib/collins_notify/adapter/hipchat.rb +92 -0
- data/lib/collins_notify/adapter/irc.rb +88 -0
- data/lib/collins_notify/application.rb +55 -0
- data/lib/collins_notify/command_runner.rb +73 -0
- data/lib/collins_notify/configuration.rb +198 -0
- data/lib/collins_notify/configuration_mixin.rb +98 -0
- data/lib/collins_notify/errors.rb +4 -0
- data/lib/collins_notify/notifier.rb +165 -0
- data/lib/collins_notify/options.rb +128 -0
- data/lib/collins_notify/version.rb +19 -0
- data/sample_config.yaml +32 -0
- data/templates/default_email.erb +11 -0
- data/templates/default_email.html.erb +8 -0
- data/templates/default_hipchat.erb +1 -0
- data/templates/default_irc.erb +1 -0
- metadata +136 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'hipchat'
|
2
|
+
|
3
|
+
module CollinsNotify
|
4
|
+
|
5
|
+
class HipchatAdapter < Notifier
|
6
|
+
register_name :hipchat
|
7
|
+
supports_mimetype :text
|
8
|
+
require_config :api_token, :from_username # who to send the message as
|
9
|
+
|
10
|
+
# Constants for coloring different messages
|
11
|
+
module HipchatStatus
|
12
|
+
SUCCESS = "green"
|
13
|
+
WARNING = "yellow"
|
14
|
+
ERROR = "red"
|
15
|
+
STATUS = "purple"
|
16
|
+
RANDOM = "random"
|
17
|
+
end
|
18
|
+
|
19
|
+
def configure!
|
20
|
+
@client = HipChat::Client.new api_token
|
21
|
+
cfg_colors = hipchat_options.fetch(:colors, {})
|
22
|
+
@colors = {
|
23
|
+
:success => cfg_colors.fetch(:success, HipchatStatus::SUCCESS),
|
24
|
+
:warning => cfg_colors.fetch(:warning, HipchatStatus::WARNING),
|
25
|
+
:error => cfg_colors.fetch(:error, HipchatStatus::ERROR),
|
26
|
+
:status => cfg_colors.fetch(:status, HipchatStatus::STATUS),
|
27
|
+
:random => cfg_colors.fetch(:random, HipchatStatus::RANDOM)
|
28
|
+
}
|
29
|
+
logger.info "Configured Hipchat client"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Bindings available
|
33
|
+
# room_name - hipchat room
|
34
|
+
# username - user message being sent as
|
35
|
+
# at - If we are at'ing a person
|
36
|
+
def notify! message_obj = OpenStruct.new, to = nil
|
37
|
+
room_name = get_room message_obj
|
38
|
+
username = get_from_username message_obj
|
39
|
+
if room_name.nil? then
|
40
|
+
raise CollinsNotify::CollinsNotifyException.new "No obj.room or hipchat.room specified"
|
41
|
+
end
|
42
|
+
message = get_message_body(binding).strip.gsub(/[\n\r]/, ' ')
|
43
|
+
message = "@#{to} #{message}" unless to.nil?
|
44
|
+
logger.info "Notifying #{room_name} from #{username} with message #{message}"
|
45
|
+
if config.test? then
|
46
|
+
logger.info "Not sending hipchat message in test mode"
|
47
|
+
return true
|
48
|
+
end
|
49
|
+
opts = client_options(message_obj)
|
50
|
+
logger.debug "Using hipchat options #{opts.inspect}"
|
51
|
+
client[room_name].send(username, message, client_options(message_obj))
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
attr_accessor :client, :colors
|
56
|
+
def api_token
|
57
|
+
hipchat_options[:api_token]
|
58
|
+
end
|
59
|
+
def color mo
|
60
|
+
if color = fetch_mo_option(mo, :color, false) then
|
61
|
+
return color
|
62
|
+
end
|
63
|
+
[:success, :warning, :error, :status].each do |key|
|
64
|
+
if fetch_mo_option(mo, key, false) then
|
65
|
+
return colors[key]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
colors[:status]
|
69
|
+
end
|
70
|
+
def client_options mo
|
71
|
+
opts = {}
|
72
|
+
opts[:notify] = true if hipchat_options[:notify]
|
73
|
+
opts[:color] = color(mo)
|
74
|
+
opts
|
75
|
+
end
|
76
|
+
def get_from_username mo
|
77
|
+
fetch_mo_option(mo, :from_username, hipchat_options[:from_username])
|
78
|
+
end
|
79
|
+
def hipchat_options
|
80
|
+
@hipchat_options ||= symbolize_hash(deep_copy_hash(config.adapters[:hipchat]))
|
81
|
+
end
|
82
|
+
def get_room mo
|
83
|
+
if config.recipient then
|
84
|
+
config.recipient
|
85
|
+
else
|
86
|
+
fetch_mo_option(mo, :room, hipchat_options[:room])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'collins_notify/adapter/helper/carried-pigeon'
|
2
|
+
|
3
|
+
module CollinsNotify
|
4
|
+
class IrcAdapter < Notifier
|
5
|
+
register_name :irc
|
6
|
+
supports_mimetype :text
|
7
|
+
require_config :username, :host, :port
|
8
|
+
|
9
|
+
def configure!
|
10
|
+
# An exception gets thrown if needed
|
11
|
+
get_channel symbolize_hash(deep_copy_hash(config.adapters[:irc])), nil
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# Available in template binding:
|
16
|
+
# message_obj - Depends on call
|
17
|
+
# nick - nickname
|
18
|
+
# channel - channel sending to
|
19
|
+
# host - host connecting to
|
20
|
+
# port - port on host being connected to
|
21
|
+
def notify! message_obj = OpenStruct.new, to = nil
|
22
|
+
tmp_config = symbolize_hash(deep_copy_hash(config.adapters[:irc]))
|
23
|
+
host = tmp_config.delete(:host)
|
24
|
+
port = tmp_config.delete(:port).to_i
|
25
|
+
nick = tmp_config.delete(:username)
|
26
|
+
channel = get_channel(tmp_config, to)
|
27
|
+
tmp_config.delete(:channel)
|
28
|
+
cp_config = {
|
29
|
+
:host => host,
|
30
|
+
:port => port,
|
31
|
+
:nick => nick,
|
32
|
+
:channel => channel,
|
33
|
+
:logger => logger
|
34
|
+
}
|
35
|
+
cp_config.merge!(tmp_config)
|
36
|
+
logger.trace "Using IRC config: #{cp_config.inspect}"
|
37
|
+
if config.test? then
|
38
|
+
logger.info "Not sending message in test mode"
|
39
|
+
return true
|
40
|
+
end
|
41
|
+
cp = try_connect cp_config
|
42
|
+
return false unless cp
|
43
|
+
logger.info "Connected to IRC"
|
44
|
+
begin
|
45
|
+
body = get_message_body(binding).strip.gsub(/[\n\r]/, ' ')
|
46
|
+
cp.message body, cp_config[:notice]
|
47
|
+
true
|
48
|
+
rescue CollinsNotify::CollinsNotifyException => e
|
49
|
+
logger.error "error sending irc notification - #{e}"
|
50
|
+
raise e
|
51
|
+
rescue Exception => e
|
52
|
+
logger.error "#{e.class.to_s} - error sending irc notification - #{e}"
|
53
|
+
raise CollinsNotify::CollinsNotifyException.new e
|
54
|
+
ensure
|
55
|
+
cp.die
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
def get_channel hash, to
|
61
|
+
if config.recipient then
|
62
|
+
make_channel config.recipient
|
63
|
+
elsif to then
|
64
|
+
make_channel to
|
65
|
+
elsif hash[:channel] then
|
66
|
+
make_channel hash.delete(:channel)
|
67
|
+
else
|
68
|
+
raise CollinsNotify::ConfigurationError.new "No irc.channel or config.recipient specified"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def make_channel chan
|
73
|
+
name = chan.start_with?('#') ? chan : "##{chan}"
|
74
|
+
name.gsub(/[^A-Za-z0-9#]/, '_')
|
75
|
+
end
|
76
|
+
|
77
|
+
def try_connect config
|
78
|
+
begin
|
79
|
+
CarriedPigeon.new config
|
80
|
+
rescue Exception => e
|
81
|
+
logger.error "error connecting to server #{config[:host]} - #{e}"
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end # class IrcAdapter
|
87
|
+
|
88
|
+
end # module CollinsNotify
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module CollinsNotify
|
2
|
+
class Application
|
3
|
+
include CollinsNotify::ConfigurationMixin
|
4
|
+
include Collins::Util::Logging
|
5
|
+
|
6
|
+
attr_writer :config
|
7
|
+
attr_reader :logger
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@logger = get_logger :logfile => $stderr, :program => 'collins_notify'
|
11
|
+
@config = CollinsNotify::Configuration.new @logger
|
12
|
+
end
|
13
|
+
|
14
|
+
# Override ConfigMixin::#config
|
15
|
+
def config
|
16
|
+
@config
|
17
|
+
end
|
18
|
+
|
19
|
+
def configure! argv
|
20
|
+
o = CollinsNotify::Options.get_instance config
|
21
|
+
o.parse! argv
|
22
|
+
if config.logfile then
|
23
|
+
@logger = get_logger :logfile => config.logfile, :program => 'collins_notify'
|
24
|
+
end
|
25
|
+
@logger.level = config.severity
|
26
|
+
end
|
27
|
+
|
28
|
+
def notify! message_obj = OpenStruct.new, to = nil
|
29
|
+
Timeout::timeout(config.timeout) do
|
30
|
+
notifier.notify! message_obj, to
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Override ConfigurationMixin#valid?
|
35
|
+
def valid?
|
36
|
+
if config.valid? then
|
37
|
+
begin
|
38
|
+
@notifier = CollinsNotify::Notifier.get_adapter self
|
39
|
+
@notifier.configure!
|
40
|
+
rescue Exception => e
|
41
|
+
logger.error "Error configuring #{type} notification - #{e}"
|
42
|
+
pp e.backtrace
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
true
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
attr_accessor :notifier
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module CollinsNotify
|
2
|
+
class CommandRunner
|
3
|
+
|
4
|
+
def run args
|
5
|
+
app = CollinsNotify::Application.new
|
6
|
+
|
7
|
+
begin
|
8
|
+
app.configure! args
|
9
|
+
get_contact_and_asset app, true # throws exception if not configured properly
|
10
|
+
rescue SystemExit => e
|
11
|
+
exit e.status
|
12
|
+
rescue CollinsNotify::ConfigurationError => e
|
13
|
+
app.logger.fatal "Error with input option - #{e}"
|
14
|
+
exit 1
|
15
|
+
rescue StandardError => e
|
16
|
+
app.logger.fatal "Error parsing CLI options - #{e}"
|
17
|
+
$stdout.puts CollinsNotify::Options.get_instance(app.config)
|
18
|
+
exit 2
|
19
|
+
end
|
20
|
+
# This is legacy behavior
|
21
|
+
app.config.type = :email if app.config.type.nil?
|
22
|
+
|
23
|
+
unless app.valid? then
|
24
|
+
$stdout.puts CollinsNotify::Options.get_instance(app.config)
|
25
|
+
exit 3
|
26
|
+
end
|
27
|
+
|
28
|
+
app.logger.trace "Configuration -> #{app.config.to_hash.inspect}"
|
29
|
+
|
30
|
+
begin
|
31
|
+
contact, asset = get_contact_and_asset app
|
32
|
+
if !app.notify!(asset, contact) then
|
33
|
+
raise Exception.new "failed"
|
34
|
+
end
|
35
|
+
rescue Timeout::Error => e
|
36
|
+
app.logger.fatal "TIMEOUT sending notification via #{app.type}"
|
37
|
+
exit 4
|
38
|
+
rescue Exception => e
|
39
|
+
app.logger.fatal "ERROR sending notification via #{app.type} - #{e}"
|
40
|
+
exit 5
|
41
|
+
end
|
42
|
+
|
43
|
+
app.logger.info "Successfully sent #{app.type} notification"
|
44
|
+
exit 0
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
def get_contact_and_asset app, test_only = false
|
49
|
+
return [nil, OpenStruct.new] unless app.selector and !app.selector.empty?
|
50
|
+
ccfg = app.config.collins
|
51
|
+
if ccfg.nil? or ccfg.empty? then
|
52
|
+
raise CollinsNotify::ConfigurationError.new "No collins configuration but selector specified"
|
53
|
+
end
|
54
|
+
collins = Collins::Client.new ccfg.merge(:logger => app.logger)
|
55
|
+
return if test_only
|
56
|
+
asset = nil
|
57
|
+
if app.selector[:tag] then
|
58
|
+
app.logger.info "Finding collins asset using asset tag #{app.selector[:tag]}"
|
59
|
+
asset = collins.get app.selector[:tag]
|
60
|
+
else
|
61
|
+
app.logger.info "Finding collins asset using asset selector #{app.selector.inspect}"
|
62
|
+
assets = collins.find(app.selector)
|
63
|
+
app.logger.debug "Found #{assets.size} assets in collins using selector, only using first"
|
64
|
+
asset = assets.first
|
65
|
+
end
|
66
|
+
if asset.nil? then
|
67
|
+
raise CollinsNotify::ConfigurationError.new "No asset found using #{app.selector.inspect}"
|
68
|
+
end
|
69
|
+
[asset.contact, asset]
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
module CollinsNotify
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
include CollinsNotify::ConfigurationMixin
|
5
|
+
|
6
|
+
attr_reader :logger
|
7
|
+
attr_accessor :adapters
|
8
|
+
|
9
|
+
def initialize logger, opts = {}
|
10
|
+
@adapters = {}
|
11
|
+
configurable.each do |key, value|
|
12
|
+
instance_variable_set "@#{key.to_s}".to_sym, value
|
13
|
+
end
|
14
|
+
@logger = logger
|
15
|
+
merge opts
|
16
|
+
end
|
17
|
+
|
18
|
+
# Override ConfigurationMixin#config
|
19
|
+
def config
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Override ConfigurationMixin#valid?
|
24
|
+
def valid?
|
25
|
+
adapters = CollinsNotify::Notifier.adapters.keys
|
26
|
+
if type.nil? || !adapters.include?(type) then
|
27
|
+
msg = "Invalid notifier type specified. Valid notifiers: #{adapters.sort.join(', ')}"
|
28
|
+
logger.error msg
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
unless message_body? then
|
32
|
+
logger.error "No message body found in specified template or stdin"
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Shorthand for severity assignment but silently fails if too verbose
|
39
|
+
def increase_verbosity
|
40
|
+
if severity > -1 then
|
41
|
+
self.severity -= 1
|
42
|
+
if trace? then
|
43
|
+
$DEBUG = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def message_body?
|
49
|
+
template? || stdin?
|
50
|
+
end
|
51
|
+
|
52
|
+
def resolved_template
|
53
|
+
return nil unless template
|
54
|
+
if File.exists?(File.expand_path(template)) then
|
55
|
+
return File.expand_path(template)
|
56
|
+
elsif template_dir && File.exists?(File.expand_path(File.join(template_dir, template))) then
|
57
|
+
return File.expand_path(File.join(template_dir, template))
|
58
|
+
elsif File.exists?(File.expand_path(File.join(default_template_dir, template))) then
|
59
|
+
return File.expand_path(File.join(default_template_dir, template))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def stdin?
|
64
|
+
!$stdin.tty?
|
65
|
+
end
|
66
|
+
|
67
|
+
def template?
|
68
|
+
!resolved_template.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
alias_method :original_to_hash, :to_hash
|
72
|
+
def to_hash
|
73
|
+
original_to_hash.merge(:severity_name => sevname, :adapters => adapters)
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
def default_template_dir
|
78
|
+
File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'templates'))
|
79
|
+
end
|
80
|
+
|
81
|
+
# Merge opts from the constructor into here
|
82
|
+
def merge opts
|
83
|
+
opts.each do |k,v|
|
84
|
+
if configurable.key?(k.to_sym) && !v.nil? then
|
85
|
+
self.send("#{k}=".to_sym, v)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
###################################################################################
|
91
|
+
# Validators and Formatters
|
92
|
+
###################################################################################
|
93
|
+
def format_collins cfg
|
94
|
+
symbolize_hash(cfg) unless cfg.nil?
|
95
|
+
end
|
96
|
+
def valid_collins? cfg
|
97
|
+
if !cfg.nil? && cfg.is_a?(Hash) then
|
98
|
+
[:host,:username,:password].each do |key|
|
99
|
+
if !cfg.key?(key) then
|
100
|
+
return false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
true
|
104
|
+
else
|
105
|
+
false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def format_config_file file
|
110
|
+
format_file file
|
111
|
+
end
|
112
|
+
def valid_config_file? file
|
113
|
+
valid_file? file
|
114
|
+
end
|
115
|
+
|
116
|
+
def format_logfile file
|
117
|
+
format_file file
|
118
|
+
end
|
119
|
+
def valid_logfile? file
|
120
|
+
valid_file? file
|
121
|
+
end
|
122
|
+
|
123
|
+
def valid_recipient? recip
|
124
|
+
!recip.nil?
|
125
|
+
end
|
126
|
+
|
127
|
+
def valid_selector? sel
|
128
|
+
!sel.nil? && sel.is_a?(Hash) && !sel.empty?
|
129
|
+
end
|
130
|
+
|
131
|
+
def format_severity sev
|
132
|
+
sev.to_i unless sev.nil?
|
133
|
+
end
|
134
|
+
def valid_severity? sev
|
135
|
+
!sev.nil? && sev.is_a?(Fixnum) && sev >= -1 && sev <= 3
|
136
|
+
end
|
137
|
+
|
138
|
+
def format_template file
|
139
|
+
f = format_file file
|
140
|
+
if File.exists?(f) then
|
141
|
+
f
|
142
|
+
elsif File.exists?(File.expand_path(File.join(default_template_dir, file))) then
|
143
|
+
File.expand_path(File.join(default_template_dir, file))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
def valid_template? file
|
147
|
+
valid_file? file
|
148
|
+
end
|
149
|
+
|
150
|
+
def format_template_dir d
|
151
|
+
File.expand_path(d.to_s) unless d.nil?
|
152
|
+
end
|
153
|
+
def valid_template_dir? d
|
154
|
+
!d.nil? && File.exists?(d)
|
155
|
+
end
|
156
|
+
|
157
|
+
def format_template_format f
|
158
|
+
f.to_sym unless f.nil?
|
159
|
+
end
|
160
|
+
def valid_template_format? t
|
161
|
+
!t.nil? && t.is_a?(Symbol) && [:default,:html].include?(t)
|
162
|
+
end
|
163
|
+
|
164
|
+
def format_template_processor f
|
165
|
+
f.to_sym unless f.nil?
|
166
|
+
end
|
167
|
+
def valid_template_processor? t
|
168
|
+
!t.nil? && t.is_a?(Symbol) && [:default,:erb].include?(t)
|
169
|
+
end
|
170
|
+
|
171
|
+
def valid_test? t
|
172
|
+
!t.nil? && (t.is_a?(TrueClass) || t.is_a?(FalseClass))
|
173
|
+
end
|
174
|
+
|
175
|
+
def format_timeout t
|
176
|
+
t.to_i unless t.nil?
|
177
|
+
end
|
178
|
+
def valid_timeout? t
|
179
|
+
!t.nil? && t.is_a?(Fixnum) && t > 0
|
180
|
+
end
|
181
|
+
|
182
|
+
def format_type t
|
183
|
+
t.to_sym unless t.nil?
|
184
|
+
end
|
185
|
+
def valid_type? t
|
186
|
+
!t.nil? && t.is_a?(Symbol)
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
def format_file file
|
191
|
+
File.expand_path(file) unless file.nil?
|
192
|
+
end
|
193
|
+
def valid_file? file
|
194
|
+
!file.nil? && File.exists?(file) && File.readable?(file)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|