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.
@@ -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,4 @@
1
+ module CollinsNotify
2
+ class CollinsNotifyException < StandardError; end
3
+ class ConfigurationError < CollinsNotifyException; end
4
+ 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
@@ -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"