jamie 0.1.0.alpha21 → 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -1,6 +1,14 @@
1
1
  language: ruby
2
+
2
3
  rvm:
3
4
  - 1.9.3
5
+ - jruby-19mode
6
+ - ruby-head
7
+
8
+ matrix:
9
+ allow_failures:
10
+ - rvm: ruby-head
11
+
4
12
  notifications:
5
13
  irc:
6
14
  channels:
data/Rakefile CHANGED
@@ -1,24 +1,38 @@
1
1
  require 'bundler/gem_tasks'
2
- require 'cane/rake_task'
3
2
  require 'rake/testtask'
4
- require 'tailor/rake_task'
5
-
6
- desc "Run cane to check quality metrics"
7
- Cane::RakeTask.new do |cane|
8
- cane.abc_exclude = %w(
9
- Jamie::RakeTasks#define
10
- Jamie::ThorTasks#define
11
- Jamie::CLI#pry_prompts
12
- )
13
- cane.style_exclude = %w(
14
- lib/vendor/hash_recursive_merge.rb
15
- )
16
- cane.doc_exclude = %w(
17
- lib/vendor/hash_recursive_merge.rb
18
- )
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push "lib"
6
+ t.test_files = FileList['spec/**/*_spec.rb']
7
+ t.verbose = true
19
8
  end
20
9
 
21
- Tailor::RakeTask.new
10
+ task :default => [ :test ]
11
+
12
+ unless RUBY_ENGINE == 'jruby'
13
+ require 'cane/rake_task'
14
+ require 'tailor/rake_task'
15
+
16
+ desc "Run cane to check quality metrics"
17
+ Cane::RakeTask.new do |cane|
18
+ cane.abc_exclude = %w(
19
+ Jamie::RakeTasks#define
20
+ Jamie::ThorTasks#define
21
+ Jamie::CLI#pry_prompts
22
+ Jamie::Instance#synchronize_or_call
23
+ )
24
+ cane.style_exclude = %w(
25
+ lib/vendor/hash_recursive_merge.rb
26
+ )
27
+ cane.doc_exclude = %w(
28
+ lib/vendor/hash_recursive_merge.rb
29
+ )
30
+ end
31
+
32
+ Tailor::RakeTask.new
33
+
34
+ Rake::Task[:default].enhance [ :cane, :tailor ]
35
+ end
22
36
 
23
37
  desc "Display LOC stats"
24
38
  task :stats do
@@ -28,10 +42,4 @@ task :stats do
28
42
  sh "countloc -r spec"
29
43
  end
30
44
 
31
- Rake::TestTask.new do |t|
32
- t.libs.push "lib"
33
- t.test_files = FileList['spec/**/*_spec.rb']
34
- t.verbose = true
35
- end
36
-
37
- task :default => [ :test, :cane, :tailor, :stats ]
45
+ Rake::Task[:default].enhance [ :stats ]
data/jamie.gemspec CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |gem|
20
20
 
21
21
  gem.required_ruby_version = ">= 1.9.1"
22
22
 
23
+ gem.add_dependency 'celluloid'
23
24
  gem.add_dependency 'thor'
24
25
  gem.add_dependency 'net-ssh'
25
26
  gem.add_dependency 'net-scp'
data/lib/jamie.rb CHANGED
@@ -18,6 +18,7 @@
18
18
 
19
19
  require 'base64'
20
20
  require 'benchmark'
21
+ require 'celluloid'
21
22
  require 'delegate'
22
23
  require 'digest'
23
24
  require 'erb'
@@ -29,6 +30,7 @@ require 'net/https'
29
30
  require 'net/scp'
30
31
  require 'net/ssh'
31
32
  require 'pathname'
33
+ require 'thread'
32
34
  require 'socket'
33
35
  require 'stringio'
34
36
  require 'vendor/hash_recursive_merge'
@@ -41,6 +43,8 @@ module Jamie
41
43
  class << self
42
44
 
43
45
  attr_accessor :logger
46
+ attr_accessor :crashes
47
+ attr_accessor :mutex
44
48
 
45
49
  # Returns the root path of the Jamie gem source code.
46
50
  #
@@ -49,10 +53,15 @@ module Jamie
49
53
  @source_root ||= Pathname.new(File.expand_path('../../', __FILE__))
50
54
  end
51
55
 
56
+ def crashes?
57
+ ! crashes.empty?
58
+ end
59
+
52
60
  def default_logger
53
61
  env_log = ENV['JAMIE_LOG'] && ENV['JAMIE_LOG'].downcase.to_sym
62
+ env_log = Util.to_logger_level(env_log) unless env_log.nil?
54
63
 
55
- Logger.new(:console => STDOUT, :level => env_log)
64
+ Logger.new(:stdout => STDOUT, :level => env_log)
56
65
  end
57
66
  end
58
67
 
@@ -101,9 +110,7 @@ module Jamie
101
110
  # @return [Array<Instance>] all instances, resulting from all platform and
102
111
  # suite combinations
103
112
  def instances
104
- @instances ||= Collection.new(suites.map { |suite|
105
- platforms.map { |platform| new_instance(suite, platform) }
106
- }.flatten)
113
+ instances_array(load_instances)
107
114
  end
108
115
 
109
116
  # @return [String] path to the Jamie YAML file
@@ -162,6 +169,24 @@ module Jamie
162
169
 
163
170
  private
164
171
 
172
+ def load_instances
173
+ return @instance_count if @instance_count && @instance_count > 0
174
+
175
+ results = []
176
+ suites.product(platforms).each_with_index do |arr, index|
177
+ results << new_instance(arr[0], arr[1], index)
178
+ end
179
+ @instance_count = results.size
180
+ end
181
+
182
+ def instances_array(instance_count)
183
+ results = []
184
+ instance_count.times do |index|
185
+ results << Celluloid::Actor["instance_#{index}".to_sym]
186
+ end
187
+ Collection.new(results)
188
+ end
189
+
165
190
  def new_suite(hash)
166
191
  path_hash = {
167
192
  :data_bags_path => calculate_path("data_bags", hash[:name]),
@@ -182,19 +207,24 @@ module Jamie
182
207
  Driver.for_plugin(hash[:driver_plugin], hash[:driver_config])
183
208
  end
184
209
 
185
- def new_instance(suite, platform)
186
- log_root = File.expand_path(File.join(jamie_root, ".jamie", "logs"))
210
+ def new_instance(suite, platform, index)
187
211
  platform_hash = platform_driver_hash(platform.name)
188
212
  driver = new_driver(merge_driver_hash(platform_hash))
189
213
  FileUtils.mkdir_p(log_root)
190
214
 
191
- Instance.new(
215
+ supervisor = Instance.supervise_as(
216
+ "instance_#{index}".to_sym,
192
217
  :suite => suite,
193
218
  :platform => platform,
194
219
  :driver => driver,
195
220
  :jr => Jr.new(suite.name),
196
- :logger => new_instance_logger(log_root)
221
+ :logger => new_instance_logger(index)
197
222
  )
223
+ supervisor.actors.first
224
+ end
225
+
226
+ def log_root
227
+ File.expand_path(File.join(jamie_root, ".jamie", "logs"))
198
228
  end
199
229
 
200
230
  def platform_driver_hash(platform_name)
@@ -203,13 +233,14 @@ module Jamie
203
233
  h.select { |key, value| [ :driver_plugin, :driver_config ].include?(key) }
204
234
  end
205
235
 
206
- def new_instance_logger(log_root)
236
+ def new_instance_logger(index)
207
237
  level = Util.to_logger_level(self.log_level)
238
+ color = Color::COLORS[index % Color::COLORS.size].to_sym
208
239
 
209
240
  lambda do |name|
210
241
  logfile = File.join(log_root, "#{name}.log")
211
242
 
212
- Logger.new(:stdout => STDOUT, :logdev => logfile,
243
+ Logger.new(:stdout => STDOUT, :color => color, :logdev => logfile,
213
244
  :level => level, :progname => name)
214
245
  end
215
246
  end
@@ -276,6 +307,31 @@ module Jamie
276
307
  # Default log level verbosity
277
308
  DEFAULT_LOG_LEVEL = :info
278
309
 
310
+ module Color
311
+ ANSI = {
312
+ :reset => 0, :black => 30, :red => 31, :green => 32, :yellow => 33,
313
+ :blue => 34, :magenta => 35, :cyan => 36, :white => 37,
314
+ :bright_black => 30, :bright_red => 31, :bright_green => 32,
315
+ :bright_yellow => 33, :bright_blue => 34, :bright_magenta => 35,
316
+ :bright_cyan => 36, :bright_white => 37
317
+ }.freeze
318
+
319
+ COLORS = %w(
320
+ cyan yellow green magenta red blue bright_cyan bright_yellow
321
+ bright_green bright_magenta bright_red, bright_blue
322
+ ).freeze
323
+
324
+ def self.escape(name)
325
+ return "" if name.nil?
326
+ return "" unless ansi = ANSI[name]
327
+ "\e[#{ansi}m"
328
+ end
329
+
330
+ def self.colorize(str, name)
331
+ "#{escape(name)}#{str}#{escape(:reset)}"
332
+ end
333
+ end
334
+
279
335
  # Logging implementation for Jamie. By default the console/stdout output will
280
336
  # be displayed differently than the file log output. Therefor, this class
281
337
  # wraps multiple loggers that conform to the stdlib `Logger` class behavior.
@@ -286,10 +342,12 @@ module Jamie
286
342
  include ::Logger::Severity
287
343
 
288
344
  def initialize(options = {})
345
+ color = options[:color] || :bright_white
346
+
289
347
  @loggers = []
290
348
  @loggers << logdev_logger(options[:logdev]) if options[:logdev]
291
- @loggers << stdout_logger(options[:stdout]) if options[:stdout]
292
- @loggers << stdout_logger(STDOUT) if @loggers.empty?
349
+ @loggers << stdout_logger(options[:stdout], color) if options[:stdout]
350
+ @loggers << stdout_logger(STDOUT, color) if @loggers.empty?
293
351
 
294
352
  self.progname = options[:progname] || "Jamie"
295
353
  self.level = options[:level] || default_log_level
@@ -318,10 +376,10 @@ module Jamie
318
376
  Util.to_logger_level(Jamie::DEFAULT_LOG_LEVEL)
319
377
  end
320
378
 
321
- def stdout_logger(stdout)
379
+ def stdout_logger(stdout, color)
322
380
  logger = StdoutLogger.new(stdout)
323
381
  logger.formatter = proc do |severity, datetime, progname, msg|
324
- "#{msg}\n"
382
+ Color.colorize("#{msg}\n", color)
325
383
  end
326
384
  logger
327
385
  end
@@ -500,8 +558,13 @@ module Jamie
500
558
  # @author Fletcher Nichol <fnichol@nichol.ca>
501
559
  class Instance
502
560
 
561
+ include Celluloid
503
562
  include Logging
504
563
 
564
+ class << self
565
+ attr_accessor :mutexes
566
+ end
567
+
505
568
  # @return [Suite] the test suite configuration
506
569
  attr_reader :suite
507
570
 
@@ -538,6 +601,7 @@ module Jamie
538
601
  @logger = logger.is_a?(Proc) ? logger.call(name) : logger
539
602
 
540
603
  @driver.instance = self
604
+ setup_driver_mutex
541
605
  end
542
606
 
543
607
  # @return [String] name of this instance
@@ -643,7 +707,7 @@ module Jamie
643
707
  destroy if destroy_mode == :passing
644
708
  end
645
709
  info "Finished testing #{to_str} (#{elapsed.real} seconds)."
646
- self
710
+ Actor.current
647
711
  ensure
648
712
  destroy if destroy_mode == :always
649
713
  end
@@ -670,6 +734,15 @@ module Jamie
670
734
  end
671
735
  end
672
736
 
737
+ def setup_driver_mutex
738
+ if driver.class.serial_actions
739
+ Jamie.mutex.synchronize do
740
+ self.class.mutexes ||= Hash.new
741
+ self.class.mutexes[driver.class] = Mutex.new
742
+ end
743
+ end
744
+ end
745
+
673
746
  def transition_to(desired)
674
747
  result = nil
675
748
  FSM.actions(last_action, desired).each do |transition|
@@ -704,13 +777,13 @@ module Jamie
704
777
  info("Finished #{output_verb.downcase} #{to_str}" +
705
778
  " (#{elapsed.real} seconds).")
706
779
  yield if block_given?
707
- self
780
+ Actor.current
708
781
  end
709
782
 
710
- def action(what)
783
+ def action(what, &block)
711
784
  state = load_state
712
785
  elapsed = Benchmark.measure do
713
- yield state if block_given?
786
+ synchronize_or_call(what, state, &block)
714
787
  end
715
788
  state[:last_action] = what.to_s
716
789
  elapsed
@@ -718,6 +791,18 @@ module Jamie
718
791
  dump_state(state)
719
792
  end
720
793
 
794
+ def synchronize_or_call(what, state, &block)
795
+ if Array(driver.class.serial_actions).include?(what)
796
+ debug("#{to_str} is synchronizing on #{driver.class}##{what}")
797
+ self.class.mutexes[driver.class].synchronize do
798
+ debug("#{to_str} is messaging #{driver.class}##{what}")
799
+ block.call(state)
800
+ end
801
+ else
802
+ block.call(state)
803
+ end
804
+ end
805
+
721
806
  def load_state
722
807
  if File.exists?(statefile)
723
808
  Util.symbolized_hash(YAML.load_file(statefile))
@@ -1037,6 +1122,10 @@ module Jamie
1037
1122
 
1038
1123
  attr_writer :instance
1039
1124
 
1125
+ class << self
1126
+ attr_reader :serial_actions
1127
+ end
1128
+
1040
1129
  def initialize(config = {})
1041
1130
  @config = config
1042
1131
  self.class.defaults.each do |attr, value|
@@ -1096,6 +1185,9 @@ module Jamie
1096
1185
 
1097
1186
  attr_reader :config, :instance
1098
1187
 
1188
+ ACTION_METHODS = %w{create converge setup verify destroy}.
1189
+ map(&:to_sym).freeze
1190
+
1099
1191
  def logger
1100
1192
  instance.logger
1101
1193
  end
@@ -1122,6 +1214,17 @@ module Jamie
1122
1214
  def self.default_config(attr, value)
1123
1215
  defaults[attr] = value
1124
1216
  end
1217
+
1218
+ def self.no_parallel_for(*methods)
1219
+ Array(methods).each do |meth|
1220
+ if ! ACTION_METHODS.include?(meth)
1221
+ raise ArgumentError, "##{meth} is not a whitelisted method."
1222
+ end
1223
+ end
1224
+
1225
+ @serial_actions ||= []
1226
+ @serial_actions += methods
1227
+ end
1125
1228
  end
1126
1229
 
1127
1230
  # Base class for a driver that uses SSH to communication with an instance.
@@ -1435,4 +1538,15 @@ module Jamie
1435
1538
  end
1436
1539
  end
1437
1540
 
1541
+ # Initialize the base logger and use that for Celluloid's logger
1438
1542
  Jamie.logger = Jamie.default_logger
1543
+ Celluloid.logger = Jamie.logger
1544
+
1545
+ # Setup a collection of instance crash exceptions for error reporting
1546
+ Jamie.crashes = []
1547
+ Celluloid.exception_handler do |exception|
1548
+ Jamie.logger.debug("An instance crashed because of #{exception.inspect}")
1549
+ Jamie.crashes << exception
1550
+ end
1551
+
1552
+ Jamie.mutex = Mutex.new
data/lib/jamie/cli.rb CHANGED
@@ -35,6 +35,7 @@ module Jamie
35
35
  # Constructs a new instance.
36
36
  def initialize(*args)
37
37
  super
38
+ $stdout.sync = true
38
39
  @config = Jamie::Config.new(ENV['JAMIE_YAML'])
39
40
  end
40
41
 
@@ -46,9 +47,11 @@ module Jamie
46
47
 
47
48
  [:create, :converge, :setup, :verify, :destroy].each do |action|
48
49
  desc(
49
- "#{action} [(all|<REGEX>)]",
50
+ "#{action} [(all|<REGEX>)] [opts]",
50
51
  "#{action.capitalize} one or more instances"
51
52
  )
53
+ method_option :parallel, :aliases => "-p", :type => :boolean,
54
+ :desc => "Perform action against all matching instances in parallel"
52
55
  define_method(action) { |*args| exec_action(action) }
53
56
  end
54
57
 
@@ -63,17 +66,26 @@ module Jamie
63
66
  * always: instances will always be destroyed afterwards.\n
64
67
  * never: instances will never be destroyed afterwards.
65
68
  DESC
69
+ method_option :parallel, :aliases => "-p", :type => :boolean,
70
+ :desc => "Perform action against all matching instances in parallel"
66
71
  method_option :destroy, :aliases => "-d", :default => "passing",
67
72
  :desc => "Destroy strategy to use after testing (passing, always, never)."
68
73
  def test(*args)
74
+ if ! %w{passing always never}.include?(options[:destroy])
75
+ raise ArgumentError, "Destroy mode must be passing, always, or never."
76
+ end
77
+
69
78
  banner "Starting Jamie"
70
79
  elapsed = Benchmark.measure do
71
- destroy_mode = options[:destroy]
72
- if ! %w{passing always never}.include?(options[:destroy])
73
- raise ArgumentError, "Destroy mode must be passing, always, or never."
80
+ destroy_mode = options[:destroy].to_sym
81
+ @task = :test
82
+ results = parse_subcommand(args.first)
83
+
84
+ if options[:parallel]
85
+ run_parallel(results, destroy_mode)
86
+ else
87
+ run_serial(results, destroy_mode)
74
88
  end
75
- result = parse_subcommand(args.first)
76
- Array(result).each { |instance| instance.test(destroy_mode.to_sym) }
77
89
  end
78
90
  banner "Jamie is finished. (#{elapsed.real} seconds)"
79
91
  end
@@ -132,12 +144,21 @@ module Jamie
132
144
  banner "Starting Jamie"
133
145
  elapsed = Benchmark.measure do
134
146
  @task = action
135
- result = parse_subcommand(args.first)
136
- Array(result).each { |instance| instance.send(task) }
147
+ results = parse_subcommand(args.first)
148
+ options[:parallel] ? run_parallel(results) : run_serial(results)
137
149
  end
138
150
  banner "Jamie is finished. (#{elapsed.real} seconds)"
139
151
  end
140
152
 
153
+ def run_serial(instances, *args)
154
+ Array(instances).map { |i| i.public_send(task, *args) }
155
+ end
156
+
157
+ def run_parallel(instances, *args)
158
+ futures = Array(instances).map { |i| i.future.public_send(task) }
159
+ futures.map { |i| i.value }
160
+ end
161
+
141
162
  def parse_subcommand(arg = nil)
142
163
  arg == "all" ? get_all_instances : get_filtered_instances(arg)
143
164
  end
data/lib/jamie/version.rb CHANGED
@@ -18,5 +18,5 @@
18
18
 
19
19
  module Jamie
20
20
 
21
- VERSION = "0.1.0.alpha21"
21
+ VERSION = "0.1.0.beta1"
22
22
  end
data/spec/jamie_spec.rb CHANGED
@@ -28,8 +28,10 @@ end
28
28
  SimpleCov.start 'gem'
29
29
 
30
30
  require 'fakefs/spec_helpers'
31
+ require 'logger'
31
32
  require 'minitest/autorun'
32
33
  require 'ostruct'
34
+ require 'stringio'
33
35
 
34
36
  require 'jamie'
35
37
  require 'jamie/driver/dummy'
@@ -391,6 +393,10 @@ describe Jamie::Instance do
391
393
 
392
394
  let(:instance) { Jamie::Instance.new(opts) }
393
395
 
396
+ before do
397
+ Celluloid.logger = Logger.new(StringIO.new)
398
+ end
399
+
394
400
  it "raises an ArgumentError if suite is missing" do
395
401
  opts.delete(:suite)
396
402
  proc { Jamie::Instance.new(opts) }.must_raise ArgumentError
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jamie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha21
4
+ version: 0.1.0.beta1
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-09 00:00:00.000000000 Z
12
+ date: 2013-01-12 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: celluloid
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
14
30
  - !ruby/object:Gem::Dependency
15
31
  name: thor
16
32
  requirement: !ruby/object:Gem::Requirement