sonar_connector 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +18 -0
  3. data/Rakefile +41 -0
  4. data/VERSION +1 -0
  5. data/bin/sonar-connector +69 -0
  6. data/config/config.example.json +82 -0
  7. data/lib/sonar_connector.rb +40 -0
  8. data/lib/sonar_connector/commands/command.rb +21 -0
  9. data/lib/sonar_connector/commands/commit_seppuku_command.rb +15 -0
  10. data/lib/sonar_connector/commands/increment_status_value_command.rb +14 -0
  11. data/lib/sonar_connector/commands/send_admin_email_command.rb +12 -0
  12. data/lib/sonar_connector/commands/update_disk_usage_command.rb +13 -0
  13. data/lib/sonar_connector/commands/update_status_command.rb +16 -0
  14. data/lib/sonar_connector/config.rb +166 -0
  15. data/lib/sonar_connector/connectors/base.rb +243 -0
  16. data/lib/sonar_connector/connectors/dummy_connector.rb +17 -0
  17. data/lib/sonar_connector/connectors/seppuku_connector.rb +26 -0
  18. data/lib/sonar_connector/consumer.rb +94 -0
  19. data/lib/sonar_connector/controller.rb +164 -0
  20. data/lib/sonar_connector/emailer.rb +16 -0
  21. data/lib/sonar_connector/rspec/spec_helper.rb +61 -0
  22. data/lib/sonar_connector/status.rb +43 -0
  23. data/lib/sonar_connector/utils.rb +39 -0
  24. data/script/console +10 -0
  25. data/spec/sonar_connector/commands/command_spec.rb +34 -0
  26. data/spec/sonar_connector/commands/commit_seppuku_command_spec.rb +25 -0
  27. data/spec/sonar_connector/commands/increment_status_value_command_spec.rb +25 -0
  28. data/spec/sonar_connector/commands/send_admin_email_command_spec.rb +14 -0
  29. data/spec/sonar_connector/commands/update_disk_usage_command_spec.rb +21 -0
  30. data/spec/sonar_connector/commands/update_status_command_spec.rb +24 -0
  31. data/spec/sonar_connector/config_spec.rb +93 -0
  32. data/spec/sonar_connector/connectors/base_spec.rb +207 -0
  33. data/spec/sonar_connector/connectors/dummy_connector_spec.rb +22 -0
  34. data/spec/sonar_connector/connectors/seppuku_connector_spec.rb +37 -0
  35. data/spec/sonar_connector/consumer_spec.rb +116 -0
  36. data/spec/sonar_connector/controller_spec.rb +46 -0
  37. data/spec/sonar_connector/emailer_spec.rb +36 -0
  38. data/spec/sonar_connector/status_spec.rb +78 -0
  39. data/spec/sonar_connector/utils_spec.rb +62 -0
  40. data/spec/spec.opts +2 -0
  41. data/spec/spec_helper.rb +6 -0
  42. metadata +235 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Trampoline Systems Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,18 @@
1
+ Trampoline SONAR Connector framework
2
+
3
+ == Dependencies
4
+
5
+ === Jruby
6
+
7
+ Here's how to install jruby 1.4.0 on OS X:
8
+
9
+ wget http://jruby.kenai.com/downloads/1.4.0/jruby-bin-1.4.0.tar.gz
10
+ sudo tar xfvz jruby-bin-1.4.0.tar.gz -C /usr/local/
11
+ ln -s /usr/local/jruby/bin/jruby /usr/bin/jruby
12
+ ln -s /usr/local/jruby/bin/rake /usr/bin/jrake
13
+ ln -s /usr/local/jruby/bin/gem /usr/bin/jgem
14
+ ln -s /usr/local/jruby/bin/jirb /usr/bin/jirb
15
+
16
+ == Copyright
17
+
18
+ Copyright (c) 2010 Trampoline Systems Ltd. See LICENSE for details.
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "sonar_connector"
8
+ gem.summary = %Q{A behind-the-firewall connector for Trampoline SONAR}
9
+ gem.description = %Q{Framework that allows arbitrary push and pull connectors to send data to an instance of the Trampoline SONAR server}
10
+ gem.email = "hello@empire42.com"
11
+ gem.homepage = "http://github.com/trampoline/sonar-connector"
12
+ gem.authors = ["Peter MacRobert", "Mark Meyer"]
13
+
14
+ gem.add_dependency "actionmailer", "= 2.3.10"
15
+ gem.add_dependency "actionmailer_extensions", ">= 0.4.2"
16
+ gem.add_dependency "json_pure", ">= 1.2.2"
17
+ gem.add_dependency "uuidtools", ">= 2.1.1"
18
+ gem.add_dependency "sonar_connector_filestore", ">= 0.1.0"
19
+
20
+ gem.add_development_dependency "rspec", ">= 1.2.8"
21
+ gem.add_development_dependency "rr", ">= 0.10.5"
22
+ end
23
+ Jeweler::GemcutterTasks.new
24
+ rescue LoadError
25
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
26
+ end
27
+
28
+ require 'spec/rake/spectask'
29
+ Spec::Rake::SpecTask.new(:spec) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.spec_files = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
35
+ spec.libs << 'lib' << 'spec'
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+ task :spec => :check_dependencies
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.8.5
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'sonar_connector')
4
+ require 'optparse'
5
+
6
+ config_filename = File.expand_path File.join(Dir.pwd, "config", "config.json")
7
+ mode = nil
8
+ install_path = nil
9
+
10
+ ARGV.options do |opts|
11
+ script_name = File.basename($0)
12
+
13
+ opts.banner = "Usage: #{script_name} OPTION"
14
+
15
+ opts.separator "Run modes:"
16
+ opts.on("--start", "Start the connector") { mode = :start }
17
+ opts.on("--check", "Validate the connector config") { mode = :check }
18
+ opts.on("--install=PATH", String, "Install the connector working dir and default config to the file system") {|p|
19
+ install_path = p
20
+ mode = :install
21
+ }
22
+ opts.on("--console", "Run IRB console in connector framework environment") { mode = :console }
23
+
24
+ opts.separator "Options:"
25
+
26
+ opts.on("-c", "--config=FILE", String, "Override the path to the config file.") {|s| config_filename = File.expand_path s}
27
+
28
+ opts.separator "Misc:"
29
+ opts.on_tail("-v", "--version", "Show version") { mode = :version }
30
+ opts.on_tail("-h", "--help", "Show this message")
31
+ opts.parse!
32
+ end
33
+
34
+ case mode
35
+ when :start
36
+ puts "Starting SONAR Connector from config file: #{config_filename}"
37
+ connector = Sonar::Connector::Controller.new(config_filename)
38
+ puts "Connector bootstrapped successfully, check log files for details."
39
+ connector.start
40
+ exit
41
+
42
+ when :check
43
+ puts "Checking config file: #{config_filename}"
44
+ Sonar::Connector::Controller.new(config_filename)
45
+ puts "...clean."
46
+ exit
47
+ when :install
48
+ path = File.expand_path install_path
49
+
50
+ if File.directory?(path)
51
+ puts "Error: Directory '#{path}' already exists, aborting."
52
+ exit(1)
53
+ end
54
+
55
+ %W{config log var}.each {|dir| FileUtils.mkdir_p File.join(path, dir)}
56
+ FileUtils.cp File.join(Sonar::Connector::ROOT, '..', "config", "config.example.json"), File.join(path, 'config', 'config.json')
57
+ puts "Success: Set up working directory '#{path}' and associated subdirs."
58
+ exit
59
+ when :console
60
+ lib_path = File.expand_path File.join(File.dirname(__FILE__), '..', 'lib')
61
+ Kernel.system "irb -rubygems -I #{lib_path} -r sonar_connector.rb"
62
+ exit
63
+ when :version
64
+ version_file = File.join File.expand_path(File.dirname(__FILE__)), "..", "VERSION"
65
+ puts "SONAR Connector Framework #{File.read(version_file)}"
66
+ exit
67
+ else
68
+ puts ARGV.options
69
+ end
@@ -0,0 +1,82 @@
1
+ {
2
+ /* log level must be one of: "debug", "info", "warn", "error", "fatal" */
3
+ "log_level" : "debug",
4
+
5
+ /* max log file size in megabytes */
6
+ "log_file_max_size" : "10",
7
+
8
+ /* number of log files to keep */
9
+ "log_files_to_keep" : "7",
10
+
11
+ "email_settings": {
12
+ "admin_recipients": ["admin@server.local"],
13
+ "admin_sender": "Sonar Connector <noreply@server.local>",
14
+ "perform_deliveries": false,
15
+
16
+ /* options are ["smtp", "sendmail", "test"] */
17
+ "delivery_method": "smtp",
18
+ "save_emails_to_disk": true,
19
+
20
+ "smtp_settings": {
21
+ "address": "127.0.0.1",
22
+ "port": 25,
23
+ "domain": "server.local",
24
+ "user_name": null,
25
+ "password": null,
26
+
27
+ /* options are ["plain", "login", "cram_md5"] */
28
+ "authentication": null
29
+ },
30
+
31
+ "sendmail_settings": {
32
+ "location": "/usr/sbin/sendmail",
33
+ "arguments": "-i -t -f nobody@localhost"
34
+ }
35
+ },
36
+
37
+ /*
38
+ Specific configuration for each connector. Each connector must have
39
+ a class and a unique name. The require load path can also be specified if necessary.
40
+ Note that each connector type may have further configuration options
41
+ that are specific to the connector class.
42
+ */
43
+ "connectors": [
44
+ {
45
+ "class": "Sonar::Connector::ImapPullConnector",
46
+ "require": "sonar_imap_pull_connector",
47
+ "name": "gmail_1",
48
+ "repeat_delay": 10,
49
+ "host": "imap.gmail.com",
50
+ "user": "foo@bar.com",
51
+ "password": "---",
52
+ "folders": "[Gmail]/All Mail"
53
+ }
54
+ ,
55
+ {
56
+ "class": "Sonar::Connector::ImapPullConnector",
57
+ "require": "sonar_imap_pull_connector",
58
+ "name": "gmail_2",
59
+ "repeat_delay": 10,
60
+ "host": "imap.gmail.com",
61
+ "user": "baz@bar.com",
62
+ "password": "---",
63
+ "folders": "[Google Mail]/All Mail"
64
+ }
65
+ ,
66
+ {
67
+ "class": "Sonar::Connector::SonarPushConnector",
68
+ "require": "sonar_push_connector",
69
+ "name": "sonar_push",
70
+ "repeat_delay": 10,
71
+ "source_connectors": ["gmail_1", "gmail_2"],
72
+ "uri": "http://localhost:3000/api/1_0/rfc822_messages",
73
+ "connector_credentials": "---"
74
+ },
75
+ {
76
+ "class": "Sonar::Connector::SeppukuConnector",
77
+ "name": "seppuku",
78
+ "enabled": true,
79
+ "repeat_delay": 43200
80
+ }
81
+ ]
82
+ }
@@ -0,0 +1,40 @@
1
+ module Sonar
2
+ module Connector
3
+ ROOT = File.dirname(__FILE__) unless Sonar::Connector.const_defined?("ROOT")
4
+ end
5
+ end
6
+
7
+ require 'rubygems'
8
+ $:.unshift(File.expand_path("..", __FILE__))
9
+
10
+ # Load external deps
11
+ require 'active_support'
12
+ require 'json'
13
+ require 'yaml'
14
+ require 'thread'
15
+ require 'logger'
16
+ require 'action_mailer'
17
+ require 'actionmailer_extensions'
18
+ require 'fileutils'
19
+ require 'sonar_connector_filestore'
20
+
21
+ # Load internal classes
22
+ %W(
23
+ controller
24
+ config
25
+ status
26
+ consumer
27
+ emailer
28
+ utils
29
+ connectors/base
30
+ connectors/dummy_connector
31
+ connectors/seppuku_connector
32
+ commands/command
33
+ commands/update_status_command
34
+ commands/send_admin_email_command
35
+ commands/update_disk_usage_command
36
+ commands/increment_status_value_command
37
+ commands/commit_seppuku_command
38
+ ).each do |file|
39
+ require File.join('sonar_connector', file)
40
+ end
@@ -0,0 +1,21 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ ##
5
+ # Base command class that all commands should subclass.
6
+
7
+ class Command
8
+
9
+ attr_accessor :proc
10
+
11
+ def initialize(proc)
12
+ @proc = proc
13
+ end
14
+
15
+ def execute(context = nil)
16
+ context.instance_eval(&@proc)
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ class CommitSeppukuCommand < Sonar::Connector::Command
5
+ def initialize
6
+ l = lambda do
7
+ # controller is in scope here because we've jumped thru some serious hoops
8
+ # and shaved the hell out of a yak or three.
9
+ Thread.new {controller.shutdown_lambda.call}
10
+ end
11
+ super(l)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ class IncrementStatusValueCommand < Sonar::Connector::Command
5
+ def initialize(connector, field, value = 1)
6
+ l = lambda do
7
+ current = status[connector.name] ? status[connector.name][field].to_i : 0
8
+ status.set connector.name, field, current+value
9
+ end
10
+ super(l)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module Sonar
2
+ module Connector
3
+ class SendAdminEmailCommand < Sonar::Connector::Command
4
+ def initialize(connector, message)
5
+ l = lambda do
6
+ Sonar::Connector::Emailer.deliver_admin_message(connector, message)
7
+ end
8
+ super(l)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Sonar
2
+ module Connector
3
+ class UpdateDiskUsageCommand < Sonar::Connector::Command
4
+ def initialize(connector)
5
+ l = lambda do
6
+ du = (Sonar::Connector::Utils.du(connector.connector_dir).to_f / 1024).round
7
+ status.set connector.name, 'disk_usage', "#{du} Kb"
8
+ end
9
+ super(l)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ ACTION_OK = 'ok'
5
+ ACTION_FAILED = 'failed'
6
+
7
+ class UpdateStatusCommand < Sonar::Connector::Command
8
+ def initialize(connector, field, value)
9
+ l = lambda do
10
+ status.set connector.name, field, value
11
+ end
12
+ super(l)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,166 @@
1
+ module Sonar
2
+ module Connector
3
+ class InvalidConfig < RuntimeError; end
4
+
5
+ class Config
6
+
7
+ # base params
8
+ attr_reader :base_dir
9
+ attr_reader :log_dir
10
+ attr_reader :connectors_dir
11
+ attr_reader :controller_log_file
12
+ attr_reader :status_file
13
+ attr_reader :connectors
14
+ attr_reader :email_settings
15
+
16
+ # configurable: logger params
17
+ attr_reader :log_level
18
+ attr_reader :log_file_max_size
19
+ attr_reader :log_files_to_keep
20
+
21
+ # Entry-point for creating and setting the CONFIG instance.
22
+ # Give it a path to the JSON settings file and it'll do the rest.
23
+ def self.load(config_file)
24
+ config = Config.new(config_file).parse
25
+ Sonar::Connector.const_set("CONFIG", config)
26
+ end
27
+
28
+ # Helper method to read and parse JSON file from disk. Abstracted for testing purposes.
29
+ def self.read_json_file(config_file)
30
+ JSON.parse IO.read(config_file)
31
+ end
32
+
33
+ def initialize(config_file)
34
+ @config_file = config_file
35
+ end
36
+
37
+ def parse
38
+ @raw_config = Config.read_json_file(config_file)
39
+
40
+ # extract the core config params
41
+ @base_dir = parse_base_dir @raw_config["base_dir"]
42
+ @log_dir = File.join @base_dir, 'log'
43
+ @connectors_dir = File.join @base_dir, 'var'
44
+ @controller_log_file = File.join @log_dir, 'controller.log'
45
+ @status_file = File.join @base_dir, 'status.yml'
46
+ @log_level = parse_log_level @raw_config["log_level"]
47
+ @log_file_max_size = parse_log_file_max_size @raw_config["log_file_max_size"]
48
+ @log_files_to_keep = parse_log_files_to_keep @raw_config["log_files_to_keep"]
49
+ @email_settings = parse_email_settings @raw_config["email_settings"]
50
+
51
+ # extract each connector, locate its class and attempt to parse its config
52
+ @connectors = parse_connectors @raw_config["connectors"]
53
+
54
+ associate_connector_dependencies! @connectors
55
+
56
+ self
57
+ rescue JSON::ParserError => e
58
+ raise InvalidConfig.new("Config file #{config_file} is not in a valid JSON format. Please check the contents carefully. This is the exact error: \n#{e.message}")
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :config_file
64
+ attr_reader :raw_config
65
+
66
+ def parse_base_dir(base_dir)
67
+ d = base_dir.blank? ? File.dirname(File.dirname(@config_file)) : base_dir
68
+ raise InvalidConfig.new("#{d} not a valid directory") unless File.directory?(d)
69
+ d
70
+ end
71
+
72
+ def parse_log_level(log_level)
73
+ raise InvalidConfig.new("Config option 'log_level' is a required parameter.") if log_level.blank?
74
+ valid_log_levels = ["debug", "info", "warn", "error", "fatal"]
75
+ raise InvalidConfig.new("unknown log_level #{log_level}") unless valid_log_levels.include?(log_level)
76
+ Logger.const_get log_level.upcase
77
+ end
78
+
79
+ def parse_log_file_max_size(log_file_max_size)
80
+ raise InvalidConfig.new("invalid log_file_max_size #{log_file_max_size}") if !log_file_max_size.blank? && log_file_max_size.to_i == 0
81
+ log_file_max_size.blank? ? 10*1024*1024 : log_file_max_size.to_i*1024*1024
82
+ end
83
+
84
+ def parse_log_files_to_keep(log_files_to_keep)
85
+ raise InvalidConfig.new("invalid log_files_to_keep #{log_files_to_keep}") if !log_files_to_keep.blank? && log_files_to_keep.to_i == 0
86
+ log_files_to_keep.blank? ? 10 : log_files_to_keep.to_i
87
+ end
88
+
89
+ def parse_email_settings(settings)
90
+ ActionMailer::Base.perform_deliveries = settings["perform_deliveries"]
91
+ ActionMailer::Base.delivery_method = settings["delivery_method"].to_sym
92
+ ActionMailer::Base.raise_delivery_errors = true
93
+
94
+ # ActionMailer needs the smtp and sendmail settings hashes to have symbols for keys
95
+ ActionMailer::Base.smtp_settings = symbolise_hash_keys settings["smtp_settings"]
96
+ ActionMailer::Base.sendmail_settings = symbolise_hash_keys settings["sendmail_settings"]
97
+
98
+ ActionMailer::Base.save_emails_to_disk = settings["save_emails_to_disk"]
99
+ ActionMailer::Base.email_output_dir = File.join @base_dir, 'sent_administrator_emails'
100
+ ActionMailer::Base.safe_recipients = settings["admin_recipients"].to_a
101
+ settings
102
+ end
103
+
104
+ def parse_connectors(connectors_config)
105
+ raise InvalidConfig.new("Connector parameter must be an array and cannot be empty") unless connectors_config.instance_of?(Array) && !connectors_config.empty?
106
+
107
+ c = []
108
+ connectors_config.each do |config|
109
+ c << parse_connector(config)
110
+ end
111
+
112
+ raise InvalidConfig.new("Connector names must be unique. You supplied: #{c.map(&:name).inspect}") if c.map(&:name).uniq.size != c.size
113
+ c
114
+ end
115
+
116
+ def parse_connector(config)
117
+
118
+ # Load the require first, if specified
119
+ begin
120
+ require config["require"] unless config["require"].blank?
121
+ rescue MissingSourceFile
122
+ raise InvalidConfig.new("Error with parameter 'require' in connector settings '#{config.inspect}': require failed - check that the path is correct.")
123
+ end
124
+
125
+ # Insist that class is specified
126
+ raise InvalidConfig.new("Error with parameter 'class' in connector settings '#{config.inspect}': class must be specified.") if config["class"].blank?
127
+
128
+ # Attempt to load the class definition
129
+ begin
130
+ klass = config["class"].constantize
131
+ rescue
132
+ raise InvalidConfig.new("Error with parameter 'class' in connector settings '#{config.inspect}': could not load class.")
133
+ end
134
+
135
+ # sanity-check that the connector class subclasses the base
136
+ raise InvalidConfig.new("Connector class #{klass.name} must subclass Sonar::Connector::Base") unless klass.ancestors.include?(Sonar::Connector::Base)
137
+ klass.new(config, self)
138
+ end
139
+
140
+ def symbolise_hash_keys(hash)
141
+ return nil unless hash
142
+ hash.keys.inject({}){|acc, k| acc[k.to_sym] = hash[k]; acc}
143
+ end
144
+
145
+ # Find all connectors with "source_connectors" specified in config, and map these
146
+ # associations to the connector instances.
147
+ def associate_connector_dependencies!(connectors)
148
+ connectors.each do |connector|
149
+ source_names = [*connector.raw_config["source_connectors"]].compact
150
+ next if source_names.blank?
151
+
152
+ source_connectors = source_names.map do |source_name|
153
+ c = connectors.select{|connector2| connector2.name == source_name}.first
154
+ raise InvalidConfig.new("Connector '#{connector.name}' references a source connector '#{source_name}' but no such connector name is defined.") unless c
155
+ raise InvalidConfig.new("Connector '#{connector.name}' cannot have itself as a source connector.") if c == connector
156
+ c
157
+ end
158
+
159
+ connector.send :source_connectors=, source_connectors
160
+ end
161
+ end
162
+
163
+ end
164
+ end
165
+ end
166
+