sonar_connector 0.8.5

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.
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
@@ -0,0 +1,16 @@
1
+ module Sonar
2
+ module Connector
3
+ class Emailer < ActionMailer::Base
4
+ def admin_message(connector, message)
5
+ from Sonar::Connector::CONFIG.email_settings["admin_sender"]
6
+ recipients Sonar::Connector::CONFIG.email_settings["admin_recipients"]
7
+ subject "Admin email from Sonar Connector"
8
+ content_type "text/plain"
9
+ body <<-BODY
10
+ Admin email from Sonar Connector. The connector '#{connector.name}' sent the following message:
11
+ #{message}
12
+ BODY
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec'
2
+ require 'spec/autorun'
3
+ require 'rr'
4
+
5
+ Spec::Runner.configure do |config|
6
+ config.mock_with RR::Adapters::Rspec
7
+
8
+ config.prepend_before(:each) do
9
+
10
+ # This dir gets wiped after every spec run, so please - pretty please -
11
+ # don't change it to anything that you care about.
12
+ def base_dir
13
+ "/tmp/sonar-connector/"
14
+ end
15
+
16
+ # Note this path doesn't have to be real - it's just used to intercept calls
17
+ # to the stubbed read_json_file method on Config
18
+ def valid_config_filename
19
+ "path to a valid config file"
20
+ end
21
+
22
+ def setup_valid_config_file
23
+ @config_options = {
24
+ "log_level" => "error",
25
+ "base_dir" => base_dir,
26
+ "email_settings" => {
27
+ "admin_sender" => "noreply@example.local",
28
+ "admin_recipients" => ["admin@example.local"],
29
+ "perform_deliveries" => true,
30
+ "delivery_method" => "test",
31
+ "raise_delivery_errors" => true,
32
+ "save_emails_to_disk" => false
33
+ },
34
+
35
+ "connectors" => [
36
+ "class" => "Sonar::Connector::DummyConnector",
37
+ "name" => "dummy1",
38
+ "repeat_delay" => 10
39
+ ]
40
+ }
41
+ stub(Sonar::Connector::Config).read_json_file(valid_config_filename){@config_options}
42
+ Sonar::Connector.send(:remove_const, "CONFIG") if defined?(Sonar::Connector::CONFIG)
43
+ end
44
+
45
+ # This is slightly dangerous.
46
+ FileUtils.rm_rf(base_dir) if File.directory?(base_dir)
47
+ FileUtils.mkdir_p(base_dir)
48
+ end
49
+
50
+ end
51
+
52
+
53
+ # Creates an anonmyous throw-away class of type=parent, with an additional
54
+ # proc for defining methods on the class. Tnx @mccraigmccraig :-)
55
+ def new_anon_class(parent, name="", &proc)
56
+ klass = Class.new(parent)
57
+ mc = klass.instance_eval{ class << self ; self ; end }
58
+ mc.send(:define_method, :to_s) {name}
59
+ klass.class_eval(&proc) if proc
60
+ klass
61
+ end
@@ -0,0 +1,43 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ # Represents the status and statistics collected by various connectors
5
+ # and is responsible for accessing, updating and persisting the status YAML file.
6
+ class Status
7
+
8
+ def initialize(config)
9
+ @status_file = config.status_file
10
+ load_status
11
+ end
12
+
13
+ def load_status
14
+ @status = YAML.load_file(status_file) rescue {}
15
+ end
16
+
17
+ def save_status
18
+ File.open(status_file, 'w') { |f| f << status.to_yaml }
19
+ end
20
+
21
+ def set(group, key, value)
22
+ status[group] = {} unless status[group]
23
+ status[group][key] = value
24
+ status[group]['last_updated'] = Time.now.to_s
25
+ save_status
26
+ end
27
+
28
+ def [](group)
29
+ status[group]
30
+ end
31
+
32
+ def []=(group, hash)
33
+ status[group] = hash
34
+ end
35
+
36
+ private
37
+
38
+ attr_accessor :status_file
39
+ attr_accessor :status
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ module Sonar
2
+ module Connector
3
+ module Utils
4
+
5
+ #
6
+ # Disk usage utility. Returns amount of disk space
7
+ # used in a given folder, in bytes.
8
+ def du(dir)
9
+ raise "#{dir} is not a directory" unless File.directory?(dir)
10
+ glob = File.join(dir, "**", "*")
11
+ Dir[glob].map {|f|
12
+ File.read(f).size rescue nil
13
+ }.compact.sum
14
+ end
15
+
16
+ module_function :du
17
+
18
+ def stdout_logger(base_config)
19
+ log = Logger.new STDOUT
20
+ log.level = base_config.log_level
21
+ log.formatter = Logger::Formatter.new
22
+ log.datetime_format = "%Y-%m-%d %H:%M:%S"
23
+ log
24
+ end
25
+
26
+ module_function :stdout_logger
27
+
28
+ def disk_logger(filename, base_config)
29
+ log = Logger.new filename, base_config.log_files_to_keep, base_config.log_file_max_size
30
+ log.level = base_config.log_level
31
+ log.formatter = Logger::Formatter.new
32
+ log.datetime_format = "%Y-%m-%d %H:%M:%S"
33
+ log
34
+ end
35
+
36
+ module_function :disk_logger
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path("../../lib", __FILE__)
4
+
5
+ require 'sonar_connector'
6
+ require 'irb'
7
+
8
+ CONTROLLER = Sonar::Connector::Controller.new
9
+ CONTROLLER.prepare_connector
10
+ IRB.start
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sonar::Connector::Command do
4
+ before do
5
+ @command
6
+ end
7
+
8
+ describe "initialize" do
9
+ it "should set proc" do
10
+ proc = Proc.new {}
11
+ c = Sonar::Connector::Command.new(proc)
12
+ c.proc.should == proc
13
+ end
14
+ end
15
+
16
+ describe "execute" do
17
+ it "should be run in context" do
18
+
19
+ # create a context instance with a #do_it method
20
+ context = new_anon_class(Object, "MyContext"){
21
+ def do_it(instance)
22
+ end
23
+ }.new
24
+
25
+ # the proc to run calls #do_it on self, and passes in self, which should be one and the same
26
+ proc = Proc.new { do_it(self) }
27
+
28
+ # ensure that do_it gets called, and the self instance passed in is the context.
29
+ mock(context).do_it(context)
30
+
31
+ Sonar::Connector::Command.new(proc).execute(context)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sonar::Connector::CommitSeppukuCommand do
4
+
5
+ before do
6
+ @connector = Object.new
7
+ @controller = Object.new
8
+ stub(@connector).name{"name"}
9
+ end
10
+
11
+ it "should update the disk usage statistic" do
12
+ @command = Sonar::Connector::CommitSeppukuCommand.new
13
+
14
+ @context = Object.new
15
+ @status = Object.new
16
+ @shutdown_lambda = Object.new
17
+
18
+ mock(@context).controller{@controller}
19
+ mock(@controller).shutdown_lambda{@shutdown_lambda}
20
+ mock(@shutdown_lambda).call
21
+
22
+ @command.execute(@context)
23
+ end
24
+
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sonar::Connector::IncrementStatusValueCommand do
4
+ before do
5
+ setup_valid_config_file
6
+ @base_config = Sonar::Connector::Config.load valid_config_filename
7
+ @status = Sonar::Connector::Status.new @base_config
8
+ stub(@connector = Object.new).name{"name"}
9
+ stub(@context).status{@status}
10
+ end
11
+
12
+
13
+ it "should set value if it is nil" do
14
+ @status.set("name", "foo", nil)
15
+ Sonar::Connector::IncrementStatusValueCommand.new(@connector, "foo").execute(@context)
16
+ @status["name"]["foo"].should == 1
17
+ end
18
+
19
+ it "should increment a value" do
20
+ @status.set "name", "foo", 1
21
+ Sonar::Connector::IncrementStatusValueCommand.new(@connector, "foo").execute(@context)
22
+ @status["name"]["foo"].should == 2
23
+ end
24
+
25
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sonar::Connector::SendAdminEmailCommand do
4
+
5
+ it "should send admin email" do
6
+ @connector = Object.new
7
+ @command = Sonar::Connector::SendAdminEmailCommand.new(@connector, "message")
8
+
9
+ mock(Sonar::Connector::Emailer).deliver_admin_message(@connector, "message")
10
+
11
+ @command.execute
12
+ end
13
+
14
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sonar::Connector::UpdateDiskUsageCommand do
4
+
5
+ it "should update the disk usage statistic" do
6
+ @connector = Object.new
7
+ mock(@connector).connector_dir{"dir"}
8
+ mock(@connector).name{"name"}
9
+
10
+ mock(Sonar::Connector::Utils).du("dir"){2048}
11
+ @command = Sonar::Connector::UpdateDiskUsageCommand.new(@connector)
12
+
13
+ @context = Object.new
14
+ @status = Object.new
15
+ mock(@status).set("name", "disk_usage", "2 Kb")
16
+ mock(@context).status{@status}
17
+
18
+ @command.execute(@context)
19
+ end
20
+
21
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sonar::Connector::UpdateStatusCommand do
4
+
5
+ it "should define constants" do
6
+ Sonar::Connector::ACTION_OK
7
+ Sonar::Connector::ACTION_FAILED
8
+ end
9
+
10
+ it "should update the disk usage statistic" do
11
+ @connector = Object.new
12
+ mock(@connector).name{"name"}
13
+
14
+ @command = Sonar::Connector::UpdateStatusCommand.new(@connector, "last_operation", Sonar::Connector::ACTION_OK)
15
+
16
+ @context = Object.new
17
+ @status = Object.new
18
+ mock(@status).set("name", "last_operation", Sonar::Connector::ACTION_OK)
19
+ mock(@context).status{@status}
20
+
21
+ @command.execute(@context)
22
+ end
23
+
24
+ end
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sonar::Connector::Config do
4
+
5
+ before do
6
+ setup_valid_config_file
7
+ end
8
+
9
+ describe "self.load" do
10
+ before do
11
+ @config = Sonar::Connector::Config.load(valid_config_filename)
12
+ end
13
+
14
+ it "should return config" do
15
+ @config.should be_instance_of(Sonar::Connector::Config)
16
+ end
17
+
18
+ it "should set CONFIG constant" do
19
+ Sonar::Connector::CONFIG.should == @config
20
+ end
21
+
22
+ end
23
+
24
+ describe "parse" do
25
+ before do
26
+ @config = Sonar::Connector::Config.new(valid_config_filename).parse
27
+ end
28
+
29
+ it "should return the config instance" do
30
+ @config.should be_instance_of(Sonar::Connector::Config)
31
+ end
32
+
33
+ it "should symbolize log_level" do
34
+ @config_options["log_level"] = "error"
35
+ @config = Sonar::Connector::Config.new(valid_config_filename).parse
36
+ @config.log_level.should == Logger::ERROR
37
+ end
38
+
39
+ it "should set email settings" do
40
+ @config.email_settings.should be_instance_of(Hash)
41
+ end
42
+ end
43
+
44
+ describe "associate_connector_dependencies!" do
45
+ before do
46
+ @config = Sonar::Connector::Config.load(valid_config_filename)
47
+ @connector_klass = new_anon_class(Sonar::Connector::Base, "MyConnector")
48
+ end
49
+
50
+ def connector_with_name_and_source(name, source_name)
51
+ @connector_klass.new({'class'=>'MyConnector', 'name'=>name, 'source_connectors'=>[source_name], 'repeat_delay'=> 10}, @config)
52
+ end
53
+
54
+ it "should associate a source connector" do
55
+ connector1 = connector_with_name_and_source 'c1', nil
56
+ connector2 = connector_with_name_and_source 'c2', 'c1'
57
+
58
+ @config.send :associate_connector_dependencies!, [connector1, connector2]
59
+
60
+ connector1.source_connectors.should be_nil
61
+ connector2.source_connectors.should == [connector1]
62
+ end
63
+
64
+ it "should associate multiple source connectors correctly" do
65
+ connector1 = connector_with_name_and_source 'c1', nil
66
+ connector2 = connector_with_name_and_source 'c2', 'c1'
67
+ connector3 = connector_with_name_and_source 'c3', nil
68
+ connector4 = connector_with_name_and_source 'c4', 'c3'
69
+
70
+ @config.send :associate_connector_dependencies!, [connector1, connector2, connector3, connector4]
71
+
72
+ connector1.source_connectors.should be_nil
73
+ connector2.source_connectors.should == [connector1]
74
+ connector3.source_connectors.should be_nil
75
+ connector4.source_connectors.should == [connector3]
76
+ end
77
+
78
+ it "should raise error when source_connector doesn't exist" do
79
+ connector1 = connector_with_name_and_source 'c1', 'invalid_connector_name'
80
+ lambda{
81
+ @config.send :associate_connector_dependencies!, [connector1]
82
+ }.should raise_error(Sonar::Connector::InvalidConfig, /no such connector name is defined/)
83
+ end
84
+
85
+ it "should raise error if connector is set as its own source" do
86
+ connector1 = connector_with_name_and_source 'c1', 'c1'
87
+ lambda{
88
+ @config.send :associate_connector_dependencies!, [connector1]
89
+ }.should raise_error(Sonar::Connector::InvalidConfig, /cannot have itself as a/)
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,207 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sonar::Connector::Base do
4
+ before do
5
+ setup_valid_config_file
6
+ @base_config = Sonar::Connector::Config.load(valid_config_filename)
7
+ @connector_klass = new_anon_class(Sonar::Connector::Base, "MyConnector") {}
8
+ @config = {'class'=>'MyConnector', 'name'=>'foo', 'repeat_delay'=> 10}
9
+ @connector = @connector_klass.new(@config, @base_config)
10
+ end
11
+
12
+ describe "initialize" do
13
+ it "should set name" do
14
+ c = @connector_klass.new(@config, @base_config)
15
+ c.name.should == 'foo'
16
+ end
17
+
18
+ it "should require repeat delay of at least 1.0 second" do
19
+ @config['repeat_delay'] = nil
20
+ lambda{
21
+ @connector_klass.new(@config, @base_config)
22
+ }.should raise_error(Sonar::Connector::InvalidConfig)
23
+
24
+ @config['repeat_delay'] = 0
25
+ lambda{
26
+ @connector_klass.new(@config, @base_config)
27
+ }.should raise_error(Sonar::Connector::InvalidConfig)
28
+
29
+ @config['repeat_delay'] = 0.9
30
+ lambda{
31
+ @connector_klass.new(@config, @base_config)
32
+ }.should raise_error(Sonar::Connector::InvalidConfig)
33
+ end
34
+
35
+ it "should set blank state hash" do
36
+ c = @connector_klass.new(@config, @base_config)
37
+ c.state.should == {}
38
+ end
39
+
40
+ it "should parse config" do
41
+ mock.instance_of(@connector_klass).parse(@config){}
42
+ @connector_klass.new(@config, @base_config)
43
+ end
44
+
45
+ it "should load state after parsing config so as not to overwrite any state" do
46
+ @connector_klass = new_anon_class(Sonar::Connector::Base, "MyConnector"){
47
+ def parse(config)
48
+ state[:foo] = 'default state value from config'
49
+ end
50
+ }
51
+
52
+ mock.instance_of(@connector_klass).read_state(){
53
+ {:foo => 'overridden by state'}
54
+ }
55
+
56
+ c = @connector_klass.new(@config, @base_config)
57
+ c.state[:foo].should == 'overridden by state'
58
+ end
59
+ end
60
+
61
+ describe "read_state" do
62
+ it "should return empty hash if the file doesn't exist" do
63
+ File.exists?(@connector.send :state_file).should be_false
64
+ @connector.read_state.should == {}
65
+ end
66
+
67
+ it "should load hash from yaml file" do
68
+ mock(File).exist?(@connector.send :state_file){true}
69
+ mock(YAML).load_file(@connector.send :state_file) { {:foo=>:bar} }
70
+ @connector.read_state.should == {:foo=>:bar}
71
+ end
72
+
73
+ it "should log error and return empty hash if the yaml read throws error" do
74
+ mock(File).exist?(@connector.send :state_file){true}
75
+ mock(YAML).load_file(@connector.send :state_file) { raise "foo" }
76
+ mock(@connector.log).error(anything) do |param|
77
+ param.should match(/error loading/)
78
+ end
79
+ @connector.read_state.should == {}
80
+ end
81
+
82
+ it "should return an empty-hash if the yaml read returns a non-hash" do
83
+ mock(File).exist?(@connector.send :state_file){true}
84
+ mock(YAML).load_file(@connector.send :state_file) { false }
85
+ mock(@connector.log).error(anything) do |param|
86
+ param.should match(/error loading/)
87
+ end
88
+ @connector.read_state.should == {}
89
+ end
90
+
91
+ end
92
+
93
+ describe "load_state" do
94
+ it "should merge keys" do
95
+ @connector.state[:foo] = :bar
96
+ @connector.state[:baz] = 'old value'
97
+
98
+ @connector.save_state
99
+
100
+ @connector.state[:baz] = 'new value'
101
+
102
+ @connector.load_state
103
+
104
+ @connector.state[:foo].should == :bar
105
+ @connector.state[:baz] = 'old value'
106
+ end
107
+ end
108
+
109
+ describe "save_state" do
110
+ it "should save state to yaml" do
111
+ @connector.state[:foo] = :bar
112
+ @connector.save_state
113
+ @connector.state[:foo] = nil
114
+ @connector.load_state
115
+ @connector.state[:foo].should == :bar
116
+ end
117
+ end
118
+
119
+ describe "start" do
120
+ before do
121
+ @connector = new_anon_class(Sonar::Connector::Base, "MyConnector"){
122
+ def action
123
+ true
124
+ end
125
+ }.new(@config, @base_config)
126
+ @queue = Queue.new
127
+ stub(@connector).sleep_for(anything)
128
+
129
+ # don't switch to log file, but don't close stdout either
130
+ mock(@connector).switch_to_log_file
131
+ end
132
+
133
+ it "should peform the action once per iteration" do
134
+ mock(@connector) do
135
+ run(){true}
136
+ run(){true}
137
+ run(){false}
138
+ end
139
+
140
+ mock(@connector).action.times(2)
141
+
142
+ @connector.prepare(@queue)
143
+ @connector.start()
144
+ end
145
+
146
+ it "should catch uncaught exceptions from the action" do
147
+ mock(@connector) do
148
+ run(){true}
149
+ run(){true}
150
+ run(){false}
151
+ end
152
+
153
+ mock(@connector) do
154
+ action(){raise "uncaught exception"}
155
+ action(){true}
156
+ end
157
+
158
+ @connector.prepare(@queue)
159
+ @connector.start
160
+ end
161
+
162
+ it "should terminate on ThreadTerminator exception" do
163
+ stub(@connector).run{true}
164
+ mock(@connector).action(){raise Sonar::Connector::ThreadTerminator.new}
165
+ @connector.prepare(@queue)
166
+ @connector.start
167
+ end
168
+
169
+ it "should queue status update and disk usage commands on successful action" do
170
+ mock(@connector) do
171
+ run(){true}
172
+ run(){true}
173
+ run(){false}
174
+ end
175
+
176
+ stub(Sonar::Connector::UpdateStatusCommand).new
177
+ stub(Sonar::Connector::UpdateDiskUsageCommand).new
178
+ stub(@queue, :<<)
179
+
180
+ @connector.prepare(@queue)
181
+ @connector.start
182
+
183
+ Sonar::Connector::UpdateStatusCommand.should have_received.new(anything, 'last_action', Sonar::Connector::ACTION_OK).times(2)
184
+ Sonar::Connector::UpdateDiskUsageCommand.should have_received.new(anything).times(2)
185
+ @queue.should have_received(:<<).with(anything).times(10) # 5x commands queued on 2x action invocations
186
+ end
187
+
188
+ it "should queue error status update on unhandled action error" do
189
+ mock(@connector) do
190
+ run(){true}
191
+ run(){false}
192
+ end
193
+
194
+ mock(@connector) do
195
+ action(){raise "uncaught exception"}
196
+ end
197
+
198
+ command = Object.new
199
+ mock(Sonar::Connector::UpdateStatusCommand).new(anything, 'last_action', Sonar::Connector::ACTION_FAILED).times(1){command}
200
+ mock(@queue, :<<).with(command).times(1)
201
+ @connector.prepare(@queue)
202
+ @connector.start
203
+ end
204
+
205
+ end
206
+
207
+ end