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
@@ -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