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,98 @@
|
|
1
|
+
module CollinsNotify; module ConfigurationMixin
|
2
|
+
|
3
|
+
include Collins::Util
|
4
|
+
|
5
|
+
def config
|
6
|
+
raise NotImplementedError.new "ConfigurationMixin#config must be implemented"
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
raise NotImplementedError.new "ConfigurationMixin#valid? must be implemented"
|
11
|
+
end
|
12
|
+
|
13
|
+
def fatal?; severity <= Logger::FATAL; end
|
14
|
+
def error?; severity <= Logger::ERROR; end
|
15
|
+
def warn?; severity <= Logger::WARN; end
|
16
|
+
def info?; severity <= Logger::INFO; end
|
17
|
+
def debug?; severity <= Logger::DEBUG; end
|
18
|
+
def trace?; severity <= Logger::TRACE; end
|
19
|
+
def sevname
|
20
|
+
if trace? then
|
21
|
+
"TRACE"
|
22
|
+
else
|
23
|
+
Logger::SEV_LABEL[severity]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test?; (config.test == true); end
|
28
|
+
|
29
|
+
# Handles magic getters/setters
|
30
|
+
def method_missing m, *args, &block
|
31
|
+
kname = m.to_s.gsub(/=$/, '') # key name, remove = in case it's assignment
|
32
|
+
is_configurable = configurable.key?(kname.to_sym)
|
33
|
+
is_assignment = is_configurable && m.to_s[-1].eql?('=') && args.size > 0
|
34
|
+
|
35
|
+
# handles gets, these are the simplest
|
36
|
+
if is_configurable && !is_assignment then
|
37
|
+
get_cfg_var "@#{kname}".to_sym
|
38
|
+
elsif is_assignment then # handle set case
|
39
|
+
formatter_name = "format_#{kname}".to_sym
|
40
|
+
if args.size == 1 then
|
41
|
+
value = args.first
|
42
|
+
else
|
43
|
+
value = args
|
44
|
+
end
|
45
|
+
|
46
|
+
# Format value if a formatter exists
|
47
|
+
if respond_to?(formatter_name) then
|
48
|
+
value = send(formatter_name, value)
|
49
|
+
end
|
50
|
+
|
51
|
+
validator_name = "valid_#{kname}?".to_sym
|
52
|
+
unless respond_to?(validator_name) then
|
53
|
+
raise NotImplementedError.new "ConfigurationMixin##{validator_name} must be implemented"
|
54
|
+
end
|
55
|
+
if send(validator_name, value) then
|
56
|
+
set_cfg_var "@#{kname}".to_sym, value
|
57
|
+
else
|
58
|
+
raise CollinsNotify::ConfigurationError.new "#{kname} #{value.inspect} is not valid"
|
59
|
+
end
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_hash
|
66
|
+
configurable.inject({}) do |ret, (k,v)|
|
67
|
+
ret.update(k => get_cfg_var("@#{k.to_s}".to_sym))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
def configurable
|
73
|
+
{
|
74
|
+
:collins => {}, # collins configs
|
75
|
+
:config_file => nil,
|
76
|
+
:logfile => nil,
|
77
|
+
:recipient => nil,
|
78
|
+
:selector => {}, # optional collins asset to notify about
|
79
|
+
:severity => Logger::INFO,
|
80
|
+
:template => nil,
|
81
|
+
:template_dir => nil, # legacy needs
|
82
|
+
:template_format => :default,
|
83
|
+
:template_processor => :default,
|
84
|
+
:test => false,
|
85
|
+
:timeout => 10,
|
86
|
+
:type => nil,
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_cfg_var name
|
91
|
+
config.instance_variable_get name
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_cfg_var name, value
|
95
|
+
config.instance_variable_set name, value
|
96
|
+
end
|
97
|
+
|
98
|
+
end; end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module CollinsNotify
|
5
|
+
|
6
|
+
class Notifier
|
7
|
+
|
8
|
+
include Collins::Util
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def require_config *keys
|
12
|
+
@requires_config = keys.map{|k| k.to_sym}
|
13
|
+
end
|
14
|
+
def requires_config?
|
15
|
+
@requires_config && !@requires_config.empty?
|
16
|
+
end
|
17
|
+
def required_config_keys
|
18
|
+
@requires_config || []
|
19
|
+
end
|
20
|
+
def register_name name
|
21
|
+
@adapter_name = name
|
22
|
+
end
|
23
|
+
def adapter_name
|
24
|
+
@adapter_name
|
25
|
+
end
|
26
|
+
def supports_mimetype *mimetypes
|
27
|
+
@supported_mimetypes = mimetypes.map{|e| e.to_sym}
|
28
|
+
end
|
29
|
+
def mimetypes
|
30
|
+
@supported_mimetypes || [:text]
|
31
|
+
end
|
32
|
+
def adapters
|
33
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }.inject({}) do |ret,k|
|
34
|
+
ret.update(k.adapter_name => k)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
def get_adapter application
|
38
|
+
type = application.type
|
39
|
+
config = application.config
|
40
|
+
adapter_config = config.adapters[type]
|
41
|
+
adapter = adapters[type]
|
42
|
+
(raise CollinsNotifyException.new("Invalid notify type #{type}")) if adapter.nil?
|
43
|
+
if adapter.requires_config? then
|
44
|
+
(raise CollinsNotify::ConfigurationError.new "No config found for #{type}") if adapter_config.nil?
|
45
|
+
adapter.required_config_keys.each do |key|
|
46
|
+
unless adapter_config.key?(key) then
|
47
|
+
raise CollinsNotify::ConfigurationError.new "Missing #{type}.#{key} config key"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
(raise CollinsNotifyException.new("No config found for #{type}")) if adapter.requires_config? and adapter_config.nil?
|
52
|
+
adapter.new application
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize app
|
57
|
+
@config = app.config
|
58
|
+
@logger = app.logger
|
59
|
+
end
|
60
|
+
|
61
|
+
# Throw an exception if it cant be configured
|
62
|
+
def configure!
|
63
|
+
raise NotImplementedError.new "CollinsNotify::Notifier#configure! must be implemented"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return boolean indicating success/fail
|
67
|
+
def notify! message_obj = OpenStruct.new, to = nil
|
68
|
+
raise NotImplementedError.new "CollinsNotify::Notifier#notify! must be implemented"
|
69
|
+
end
|
70
|
+
|
71
|
+
def supports_text?
|
72
|
+
self.class.mimetypes.include?(:text)
|
73
|
+
end
|
74
|
+
def supports_html?
|
75
|
+
self.class.mimetypes.include?(:html)
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
attr_reader :config
|
80
|
+
attr_reader :logger
|
81
|
+
|
82
|
+
# Retrieve (safely) a key from a binding
|
83
|
+
def fetch_bound_option b, name, default
|
84
|
+
begin
|
85
|
+
eval(name, b)
|
86
|
+
rescue Exception => e
|
87
|
+
logger.trace "Could not find value name #{name} in binding"
|
88
|
+
default
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Retrieve (safely) a key from a message object which may be a hash, OpenStruct, or arbitrary
|
93
|
+
# value
|
94
|
+
def fetch_mo_option mo, key, default
|
95
|
+
value = default
|
96
|
+
if mo.is_a?(Hash) && mo.key?(key) && !mo[key].nil? then
|
97
|
+
value = mo[key]
|
98
|
+
elsif mo.respond_to?(key) && !mo.send(key).nil? then
|
99
|
+
value = mo.send(key)
|
100
|
+
end
|
101
|
+
value
|
102
|
+
end
|
103
|
+
|
104
|
+
# b is the binding to use
|
105
|
+
def get_message_body b
|
106
|
+
if config.stdin? then
|
107
|
+
message_txt = $stdin.read.strip
|
108
|
+
if config.template_processor == :erb then
|
109
|
+
render_template message_txt, b
|
110
|
+
else
|
111
|
+
message_txt
|
112
|
+
end
|
113
|
+
elsif config.template? then
|
114
|
+
tmpl = config.resolved_template
|
115
|
+
logger.debug "Using template file #{tmpl}"
|
116
|
+
render_template File.new(tmpl), b
|
117
|
+
else
|
118
|
+
raise CollinsNotify::CollinsNotifyException.new "Unknown message body type"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_template tmpl, b
|
123
|
+
template_format = config.template_format
|
124
|
+
if template_format == :default && tmpl.is_a?(File) && tmpl.path.include?(".html") then
|
125
|
+
logger.info "Detected HTML formatted template file, will render to HTML if possible"
|
126
|
+
# If format was unspecified but it seems like it's html, make it so
|
127
|
+
template_format = :html
|
128
|
+
end
|
129
|
+
tmpl_txt = tmpl.is_a?(File) ? tmpl.read : tmpl
|
130
|
+
begin
|
131
|
+
template = ERB.new(tmpl_txt, nil, '<>')
|
132
|
+
html_text = plain_text = nil
|
133
|
+
if template_format == :html then
|
134
|
+
logger.debug "Template format is html, rendering it as HTML"
|
135
|
+
html_text = template.result(b)
|
136
|
+
plain_text = as_plain_text html_text
|
137
|
+
else
|
138
|
+
logger.debug "Template format is plain text, treating it as such"
|
139
|
+
plain_text = template.result(b)
|
140
|
+
end
|
141
|
+
if supports_html? then
|
142
|
+
logger.info "HTML is supported by #{self.class.adapter_name}"
|
143
|
+
handle_html b, html_text, plain_text
|
144
|
+
else
|
145
|
+
logger.info "Only plain text supported by #{self.class.adapter_name}"
|
146
|
+
logger.debug "Rendered plain text: '#{plain_text.gsub(/[\r\n]/, ' ')}'"
|
147
|
+
plain_text
|
148
|
+
end
|
149
|
+
rescue Exception => e
|
150
|
+
raise CollinsNotify::CollinsNotifyException.new "Invalid template #{tmpl} - #{e}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# b is original binding, html is html version, plain_text is plain text version
|
155
|
+
def handle_html b, html, plain_text
|
156
|
+
html
|
157
|
+
end
|
158
|
+
|
159
|
+
def as_plain_text html
|
160
|
+
Nokogiri::HTML(html).text
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module CollinsNotify
|
5
|
+
|
6
|
+
class Options < OptionParser
|
7
|
+
ETC_CONFIG = "/etc/collins_notify.yaml"
|
8
|
+
|
9
|
+
include Collins::Util
|
10
|
+
|
11
|
+
def self.app_name
|
12
|
+
File.basename($0)
|
13
|
+
end
|
14
|
+
def self.get_instance config
|
15
|
+
i = CollinsNotify::Options.new config
|
16
|
+
i.banner = "Usage: #{app_name} --config=CONFIG [options]"
|
17
|
+
i.separator ""
|
18
|
+
i.instance_eval { setup }
|
19
|
+
i
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse! argv = default_argv
|
23
|
+
res = super
|
24
|
+
cfg_file = get_config_file
|
25
|
+
if cfg_file then #config.config_file then
|
26
|
+
yaml = YAML::load(File.open(cfg_file))
|
27
|
+
adapters = CollinsNotify::Notifier.adapters.keys
|
28
|
+
yaml.each do |k,v|
|
29
|
+
if adapters.include?(k.to_sym) then
|
30
|
+
config.adapters[k.to_sym] = symbolize_hash(v)
|
31
|
+
else
|
32
|
+
try_configure k, v
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
res
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
attr_reader :config
|
41
|
+
def initialize config
|
42
|
+
@config = config
|
43
|
+
super()
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_config_file
|
47
|
+
if config.config_file then
|
48
|
+
config.config_file
|
49
|
+
elsif File.exists?(ETC_CONFIG) && File.readable?(ETC_CONFIG) then
|
50
|
+
ETC_CONFIG
|
51
|
+
else
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def setup
|
57
|
+
separator "Specific options:"
|
58
|
+
on('-c', '--config=CONFIG', 'Notifier config. Must include any neccesary credentials for the specified notify type') do |config|
|
59
|
+
@config.config_file = config
|
60
|
+
end
|
61
|
+
on('--recipient=NAME', 'Email address, username, channel, etc') do |n|
|
62
|
+
@config.recipient = n
|
63
|
+
end
|
64
|
+
on('--selector=SELECTOR', 'Selector to use to retrieve collins assets') do |s|
|
65
|
+
@config.selector = eval(s)
|
66
|
+
end
|
67
|
+
on('--tag=TAG', 'Unique tag of asset') do |t|
|
68
|
+
@config.selector = {:tag => t}
|
69
|
+
end
|
70
|
+
adapters = CollinsNotify::Notifier.adapters.keys
|
71
|
+
msg = "Select notification type (#{adapters.sort.join(', ')})"
|
72
|
+
on('--type=TYPE', adapters, msg) do |t|
|
73
|
+
@config.type = t
|
74
|
+
end
|
75
|
+
separator ""
|
76
|
+
separator "Common options:"
|
77
|
+
on_tail('-d', '--debug', 'Legacy, same as -v') do
|
78
|
+
@config.increase_verbosity
|
79
|
+
end
|
80
|
+
on_tail('-h', '--help', 'This help') do
|
81
|
+
$stdout.puts self
|
82
|
+
exit 0
|
83
|
+
end
|
84
|
+
on_tail('--logfile=FILE', 'Log file to use, or stderr') do |file|
|
85
|
+
@config.logfile = file
|
86
|
+
end
|
87
|
+
on_tail('--template=TEMPLATE', 'Template to use for notifications, erb file') do |t|
|
88
|
+
@config.template = t
|
89
|
+
end
|
90
|
+
on_tail('--template-dir=DIR', 'Use fully qualified path in --template, do not use this') do |d|
|
91
|
+
@config.template_dir = d
|
92
|
+
end
|
93
|
+
on_tail('--template-format=FMT', [:default, :html], 'Template format (default, html).') do |t|
|
94
|
+
@config.template_format = t.to_sym
|
95
|
+
end
|
96
|
+
on_tail('--template-processor=PROC', [:default, :erb], 'Template processor (default, erb).') do |t|
|
97
|
+
@config.template_processor = t.to_sym
|
98
|
+
end
|
99
|
+
on_tail('-t', '--[no-]test', 'Enable or disable testing') do |t|
|
100
|
+
@config.test = t
|
101
|
+
end
|
102
|
+
on_tail('--timeout=TIMEOUT', Integer, 'Timeout in seconds, defaults to 10') do |t|
|
103
|
+
@config.timeout = t
|
104
|
+
end
|
105
|
+
on_tail('-v', '--verbose', verbose_help) do
|
106
|
+
@config.increase_verbosity
|
107
|
+
end
|
108
|
+
on_tail('-V', '--version', 'Software version') do
|
109
|
+
$stdout.puts "collins_notify #{CollinsNotify::Version}"
|
110
|
+
exit 0
|
111
|
+
end
|
112
|
+
end
|
113
|
+
def verbose_help
|
114
|
+
"Increase verbosity. Specify multiple -v options to increase (up to 4)"
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
def try_configure key, value
|
119
|
+
begin
|
120
|
+
config.send("#{key}=".to_sym, value)
|
121
|
+
rescue Exception => e
|
122
|
+
raise CollinsNotify::ConfigurationError.new "#{key} is not a valid config option"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CollinsNotify; module Version
|
2
|
+
|
3
|
+
class << self
|
4
|
+
def location= loc
|
5
|
+
@location = loc
|
6
|
+
end
|
7
|
+
def location
|
8
|
+
@location || File.absolute_path(File.join(File.dirname(__FILE__), '..', '..', 'VERSION'))
|
9
|
+
end
|
10
|
+
def to_s
|
11
|
+
if location && File.exists?(location) then
|
12
|
+
File.read(location)
|
13
|
+
else
|
14
|
+
"0.0.0-pre"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end; end
|
data/sample_config.yaml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
---
|
2
|
+
:collins:
|
3
|
+
host: "https://collins.example.net"
|
4
|
+
username: "some_username"
|
5
|
+
password: "some_password"
|
6
|
+
timeout: 30
|
7
|
+
:email:
|
8
|
+
address: "smtp.sendgrid.net"
|
9
|
+
port: 587
|
10
|
+
domain: "example.com"
|
11
|
+
user_name: "some_username"
|
12
|
+
password: "some_password"
|
13
|
+
authentication: "plain"
|
14
|
+
enable_starttls_auto: true
|
15
|
+
sender_address: "Collins <collins@example.com>"
|
16
|
+
:hipchat:
|
17
|
+
api_token: "api_token_here"
|
18
|
+
colors:
|
19
|
+
success: "green"
|
20
|
+
error: "red"
|
21
|
+
notify: true
|
22
|
+
room: "AutoBots"
|
23
|
+
from_username: "Collins"
|
24
|
+
:irc:
|
25
|
+
username: "some_username"
|
26
|
+
password: "some_password"
|
27
|
+
host: "hostname"
|
28
|
+
port: 6697
|
29
|
+
ssl: true
|
30
|
+
join: true
|
31
|
+
notice: true
|
32
|
+
channel: "#test"
|