collins_notify 0.0.1
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 +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
|