jamie 0.1.0.alpha21 → 0.1.0.beta1

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