jamie 0.1.0.beta1 → 0.1.0.beta2

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/lib/jamie.rb CHANGED
@@ -65,6 +65,38 @@ module Jamie
65
65
  end
66
66
  end
67
67
 
68
+ module Error ; end
69
+
70
+ # Base exception class from which all Jamie exceptions derive. This class
71
+ # nests an exception when this class is re-raised from a rescue block.
72
+ class StandardError < ::StandardError
73
+
74
+ include Error
75
+
76
+ attr_reader :original
77
+
78
+ def initialize(msg, original = $!)
79
+ super(msg)
80
+ @original = original
81
+ end
82
+ end
83
+
84
+ # Base exception class for all exceptions that are caused by user input
85
+ # errors.
86
+ class UserError < StandardError ; end
87
+
88
+ # Base exception class for all exceptions that are caused by incorrect use
89
+ # of an API.
90
+ class ClientError < StandardError ; end
91
+
92
+ # Base exception class for exceptions that are caused by external library
93
+ # failures which may be temporary.
94
+ class TransientFailure < StandardError ; end
95
+
96
+ # Exception class for any exceptions raised when performing an instance
97
+ # action.
98
+ class ActionFailed < TransientFailure ; end
99
+
68
100
  # Base configuration class for Jamie. This class exposes configuration such
69
101
  # as the location of the Jamie YAML file, instances, log_levels, etc.
70
102
  #
@@ -75,6 +107,7 @@ module Jamie
75
107
  attr_writer :platforms
76
108
  attr_writer :suites
77
109
  attr_writer :log_level
110
+ attr_writer :supervised
78
111
  attr_writer :test_base_path
79
112
 
80
113
  # Default path to the Jamie YAML file
@@ -126,6 +159,10 @@ module Jamie
126
159
  end
127
160
  end
128
161
 
162
+ def supervised
163
+ @supervised.nil? ? @supervised = true : @supervised
164
+ end
165
+
129
166
  # @return [String] base path that may contain a common `data_bags/`
130
167
  # directory or an instance's `data_bags/` directory
131
168
  def test_base_path
@@ -210,17 +247,28 @@ module Jamie
210
247
  def new_instance(suite, platform, index)
211
248
  platform_hash = platform_driver_hash(platform.name)
212
249
  driver = new_driver(merge_driver_hash(platform_hash))
213
- FileUtils.mkdir_p(log_root)
214
-
215
- supervisor = Instance.supervise_as(
216
- "instance_#{index}".to_sym,
250
+ actor_name = "instance_#{index}".to_sym
251
+ opts = {
217
252
  :suite => suite,
218
253
  :platform => platform,
219
254
  :driver => driver,
220
255
  :jr => Jr.new(suite.name),
221
256
  :logger => new_instance_logger(index)
222
- )
223
- supervisor.actors.first
257
+ }
258
+
259
+ FileUtils.mkdir_p(log_root)
260
+ new_instance_supervised_or_not(actor_name, opts)
261
+ end
262
+
263
+ def new_instance_supervised_or_not(actor_name, opts)
264
+ if supervised
265
+ supervisor = Instance.supervise_as(actor_name, opts)
266
+ actor = supervisor.actors.first
267
+ Jamie.logger.debug("Supervising #{actor.to_str} with #{supervisor}")
268
+ actor
269
+ else
270
+ Celluloid::Actor[actor_name] = Instance.new(opts)
271
+ end
224
272
  end
225
273
 
226
274
  def log_root
@@ -505,7 +553,7 @@ module Jamie
505
553
 
506
554
  def validate_options(opts)
507
555
  [ :name, :run_list ].each do |k|
508
- raise ArgumentError, "Attribute '#{k}' is required." if opts[k].nil?
556
+ raise ClientError, "Suite#new requires option :#{k}" if opts[k].nil?
509
557
  end
510
558
  end
511
559
  end
@@ -546,7 +594,7 @@ module Jamie
546
594
 
547
595
  def validate_options(opts)
548
596
  [ :name ].each do |k|
549
- raise ArgumentError, "Attribute '#{k}' is required." if opts[k].nil?
597
+ raise ClientError, "Platform#new requires option :#{k}" if opts[k].nil?
550
598
  end
551
599
  end
552
600
  end
@@ -726,11 +774,15 @@ module Jamie
726
774
  Kernel.exec(command, *args)
727
775
  end
728
776
 
777
+ def last_action
778
+ load_state[:last_action]
779
+ end
780
+
729
781
  private
730
782
 
731
783
  def validate_options(opts)
732
784
  [ :suite, :platform, :driver, :jr, :logger ].each do |k|
733
- raise ArgumentError, "Attribute '#{k}' is required." if opts[k].nil?
785
+ raise ClientError, "Instance#new requires option :#{k}" if opts[k].nil?
734
786
  end
735
787
  end
736
788
 
@@ -785,8 +837,12 @@ module Jamie
785
837
  elapsed = Benchmark.measure do
786
838
  synchronize_or_call(what, state, &block)
787
839
  end
788
- state[:last_action] = what.to_s
840
+ state[:last_action] = what
789
841
  elapsed
842
+ rescue ActionFailed
843
+ raise
844
+ rescue Exception => e
845
+ raise ActionFailed, "Failed to complete ##{what} action: [#{e.message}]"
790
846
  ensure
791
847
  dump_state(state)
792
848
  end
@@ -828,10 +884,6 @@ module Jamie
828
884
  ))
829
885
  end
830
886
 
831
- def last_action
832
- load_state[:last_action]
833
- end
834
-
835
887
  # The simplest finite state machine pseudo-implementation needed to manage
836
888
  # an Instance.
837
889
  #
@@ -952,7 +1004,7 @@ module Jamie
952
1004
  DEFAULT_TEST_ROOT = File.join(Dir.pwd, "test/integration").freeze
953
1005
 
954
1006
  def validate_options(suite_name)
955
- raise ArgumentError, "'suite_name' is required." if suite_name.nil?
1007
+ raise ClientError, "Jr#new requires a suite_name" if suite_name.nil?
956
1008
  end
957
1009
 
958
1010
  def install_script
@@ -1063,7 +1115,7 @@ module Jamie
1063
1115
  module ShellOut
1064
1116
 
1065
1117
  # Wrapped exception for any interally raised shell out commands.
1066
- class ShellCommandFailed < StandardError ; end
1118
+ class ShellCommandFailed < TransientFailure ; end
1067
1119
 
1068
1120
  # Executes a command in a subshell on the local running system.
1069
1121
  #
@@ -1071,6 +1123,8 @@ module Jamie
1071
1123
  # @param use_sudo [TrueClass, FalseClass] whether or not to use sudo
1072
1124
  # @param log_subject [String] used in the output or log header for clarity
1073
1125
  # and context
1126
+ # @raise [ShellCommandFailed] if the command fails
1127
+ # @raise [Error] for all other unexpected exceptions
1074
1128
  def run_command(cmd, use_sudo = false, log_subject = "local")
1075
1129
  cmd = "sudo #{cmd}" if use_sudo
1076
1130
  subject = "[#{log_subject} command]"
@@ -1082,6 +1136,9 @@ module Jamie
1082
1136
  sh.error!
1083
1137
  rescue Mixlib::ShellOut::ShellCommandFailed => ex
1084
1138
  raise ShellCommandFailed, ex.message
1139
+ rescue Exception => error
1140
+ error.extend(Jamie::Error)
1141
+ raise
1085
1142
  end
1086
1143
 
1087
1144
  private
@@ -1096,18 +1153,23 @@ module Jamie
1096
1153
 
1097
1154
  module Driver
1098
1155
 
1099
- # Wrapped exception for any internally raised driver exceptions.
1100
- class ActionFailed < StandardError ; end
1101
-
1102
1156
  # Returns an instance of a driver given a plugin type string.
1103
1157
  #
1104
1158
  # @param plugin [String] a driver plugin type, which will be constantized
1105
1159
  # @return [Driver::Base] a driver instance
1160
+ # @raise [ClientError] if a driver instance could not be created
1106
1161
  def self.for_plugin(plugin, config)
1107
1162
  require "jamie/driver/#{plugin}"
1108
1163
 
1109
- klass = self.const_get(Util.to_camel_case(plugin))
1164
+ str_const = Util.to_camel_case(plugin)
1165
+ klass = self.const_get(str_const)
1110
1166
  klass.new(config)
1167
+ rescue LoadError
1168
+ raise ClientError, "Could not require '#{plugin}' plugin from load path"
1169
+ rescue NameError
1170
+ raise ClientError, "No class 'Jamie::Driver::#{str_const}' could be found"
1171
+ rescue
1172
+ raise ClientError, "Failed to create a driver for '#{plugin}' plugin"
1111
1173
  end
1112
1174
 
1113
1175
  # Base class for a driver. A driver is responsible for carrying out the
@@ -1218,7 +1280,7 @@ module Jamie
1218
1280
  def self.no_parallel_for(*methods)
1219
1281
  Array(methods).each do |meth|
1220
1282
  if ! ACTION_METHODS.include?(meth)
1221
- raise ArgumentError, "##{meth} is not a whitelisted method."
1283
+ raise ClientError, "##{meth} is not a valid no_parallel_for method"
1222
1284
  end
1223
1285
  end
1224
1286
 
@@ -1236,7 +1298,7 @@ module Jamie
1236
1298
  class SSHBase < Base
1237
1299
 
1238
1300
  def create(state)
1239
- raise NotImplementedError, "#create must be implemented by subclass."
1301
+ raise ClientError, "#{self.class}#create must be implemented"
1240
1302
  end
1241
1303
 
1242
1304
  def converge(state)
@@ -1266,7 +1328,7 @@ module Jamie
1266
1328
  end
1267
1329
 
1268
1330
  def destroy(state)
1269
- raise NotImplementedError, "#destroy must be implemented by subclass."
1331
+ raise ClientError, "#{self.class}#destroy must be implemented"
1270
1332
  end
1271
1333
 
1272
1334
  def login_command(state)
@@ -1295,9 +1357,9 @@ module Jamie
1295
1357
  end
1296
1358
 
1297
1359
  def install_omnibus(ssh_args)
1298
- ssh(ssh_args, <<-INSTALL)
1360
+ ssh(ssh_args, <<-INSTALL.gsub(/^ {10}/, ''))
1299
1361
  if [ ! -d "/opt/chef" ] ; then
1300
- curl -L https://www.opscode.com/chef/install.sh | sudo bash
1362
+ curl -sSL https://www.opscode.com/chef/install.sh | sudo bash
1301
1363
  fi
1302
1364
  INSTALL
1303
1365
  end
@@ -1320,6 +1382,7 @@ module Jamie
1320
1382
  end
1321
1383
 
1322
1384
  def ssh(ssh_args, cmd)
1385
+ debug("[SSH] #{ssh_args[1]}@#{ssh_args[0]} (#{cmd})")
1323
1386
  Net::SSH.start(*ssh_args) do |ssh|
1324
1387
  exit_code = ssh_exec_with_exit!(ssh, cmd)
1325
1388
 
@@ -1336,6 +1399,9 @@ module Jamie
1336
1399
  def ssh_exec_with_exit!(ssh, cmd)
1337
1400
  exit_code = nil
1338
1401
  ssh.open_channel do |channel|
1402
+
1403
+ channel.request_pty
1404
+
1339
1405
  channel.exec(cmd) do |ch, success|
1340
1406
 
1341
1407
  channel.on_data do |ch, data|
@@ -1462,44 +1528,40 @@ module Jamie
1462
1528
 
1463
1529
  def prepare_tmpdir(tmpdir)
1464
1530
  if File.exists?(File.join(jamie_root, "Berksfile"))
1465
- run_berks(tmpdir)
1531
+ run_resolver("Berkshelf", "berks", tmpdir)
1466
1532
  elsif File.exists?(File.join(jamie_root, "Cheffile"))
1467
- run_librarian(tmpdir)
1533
+ run_resolver("Librarian", "librarian-chef", tmpdir)
1468
1534
  elsif File.directory?(File.join(jamie_root, "cookbooks"))
1469
1535
  cp_cookbooks(tmpdir)
1470
1536
  else
1471
1537
  FileUtils.rmtree(tmpdir)
1472
- abort "Berksfile, Cheffile or cookbooks/ must exist in #{jamie_root}"
1538
+ fatal("Berksfile, Cheffile or cookbooks/ must exist in #{jamie_root}")
1539
+ raise UserError, "Cookbooks could not be found"
1473
1540
  end
1474
1541
  end
1475
1542
 
1476
- def run_berks(tmpdir)
1543
+ def run_resolver(name, bin, tmpdir)
1477
1544
  begin
1478
- run_command "if ! command -v berks >/dev/null; then exit 1; fi"
1545
+ run_command "if ! command -v #{bin} >/dev/null; then exit 1; fi"
1479
1546
  rescue Jamie::ShellOut::ShellCommandFailed
1480
- abort ">>>>>> Berkshelf must be installed, add it to your Gemfile."
1547
+ fatal("#{name} must be installed, add it to your Gemfile.")
1548
+ raise UserError, "#{bin} command not found"
1481
1549
  end
1482
- run_command "berks install --path #{tmpdir}"
1550
+ run_command "#{bin} install --path #{tmpdir}"
1483
1551
  end
1484
1552
 
1485
- def run_librarian(tmpdir)
1486
- begin
1487
- run_command "if ! command -v librarian-chef >/dev/null; then exit 1; fi"
1488
- rescue Jamie::ShellOut::ShellCommandFailed
1489
- abort ">>>>>> Librarian must be installed, add it to your Gemfile."
1490
- end
1491
- run_command "librarian-chef install --path #{tmpdir}"
1553
+ def cp_cookbooks(tmpdir)
1554
+ FileUtils.cp_r(File.join(jamie_root, "cookbooks", "."), tmpdir)
1555
+ cp_this_cookbook(tmpdir) if File.exists?(File.expand_path('metadata.rb'))
1492
1556
  end
1493
1557
 
1494
- def cp_cookbooks(tmpdir)
1558
+ def cp_this_cookbook(tmpdir)
1495
1559
  metadata_rb = File.join(jamie_root, "metadata.rb")
1496
1560
  cb_name = MetadataChopper.extract(metadata_rb).first
1497
- abort ">>>>>> name attribute must be set in metadata.rb." if cb_name.nil?
1498
1561
  cb_path = File.join(tmpdir, cb_name)
1499
1562
  glob = Dir.glob("#{jamie_root}/{metadata.rb,README.*," +
1500
1563
  "attributes,files,libraries,providers,recipes,resources,templates}")
1501
1564
 
1502
- FileUtils.cp_r(File.join(jamie_root, "cookbooks", "."), tmpdir)
1503
1565
  FileUtils.mkdir_p(cb_path)
1504
1566
  FileUtils.cp_r(glob, cb_path)
1505
1567
  end
data/lib/jamie/cli.rb CHANGED
@@ -37,12 +37,23 @@ module Jamie
37
37
  super
38
38
  $stdout.sync = true
39
39
  @config = Jamie::Config.new(ENV['JAMIE_YAML'])
40
+ @config.supervised = false
40
41
  end
41
42
 
42
43
  desc "list [(all|<REGEX>)]", "List all instances"
44
+ method_option :bare, :aliases => "-b", :type => :boolean,
45
+ :desc => "List the name of each instance only, one per line"
43
46
  def list(*args)
44
47
  result = parse_subcommand(args.first)
45
- say Array(result).map{ |i| i.name }.join("\n")
48
+ if options[:bare]
49
+ say Array(result).map{ |i| i.name }.join("\n")
50
+ else
51
+ table = [
52
+ [ set_color("Instance", :green), set_color("Last Action", :green) ]
53
+ ]
54
+ table += Array(result).map { |i| display_instance(i) }
55
+ print_table(table)
56
+ end
46
57
  end
47
58
 
48
59
  [:create, :converge, :setup, :verify, :destroy].each do |action|
@@ -189,6 +200,18 @@ module Jamie
189
200
  result
190
201
  end
191
202
 
203
+ def display_instance(instance)
204
+ action = case instance.last_action
205
+ when :create then set_color("Created", :cyan)
206
+ when :converge then set_color("Converged", :magenta)
207
+ when :setup then set_color("Set Up", :blue)
208
+ when :verify then set_color("Verified", :yellow)
209
+ when nil then set_color("<Not Created>", :red)
210
+ else set_color("<Unknown>", :white)
211
+ end
212
+ [ set_color(instance.name, :white), action ]
213
+ end
214
+
192
215
  def die(task, msg)
193
216
  error "\n#{msg}\n\n"
194
217
  help(task)
@@ -266,7 +289,11 @@ module Jamie
266
289
  }
267
290
  end
268
291
  end.flatten
269
- cookbook_name = MetadataChopper.extract('metadata.rb').first
292
+ cookbook_name = if File.exists?(File.expand_path('metadata.rb'))
293
+ MetadataChopper.extract('metadata.rb').first
294
+ else
295
+ nil
296
+ end
270
297
  run_list = cookbook_name ? "recipe[#{cookbook_name}]" : nil
271
298
  attributes = cookbook_name ? { cookbook_name => nil } : nil
272
299
 
@@ -32,6 +32,7 @@ module Jamie
32
32
  # @yield [self] gives itself to the block
33
33
  def initialize
34
34
  @config = Jamie::Config.new
35
+ @config.supervised = false
35
36
  yield self if block_given?
36
37
  define
37
38
  end
@@ -35,6 +35,7 @@ module Jamie
35
35
  def initialize(*args)
36
36
  super
37
37
  @config = Jamie::Config.new
38
+ @config.supervised = false
38
39
  yield self if block_given?
39
40
  define
40
41
  end
data/lib/jamie/version.rb CHANGED
@@ -18,5 +18,5 @@
18
18
 
19
19
  module Jamie
20
20
 
21
- VERSION = "0.1.0.beta1"
21
+ VERSION = "0.1.0.beta2"
22
22
  end
data/spec/jamie_spec.rb CHANGED
@@ -314,12 +314,12 @@ describe Jamie::Suite do
314
314
 
315
315
  it "raises an ArgumentError if name is missing" do
316
316
  opts.delete(:name)
317
- proc { Jamie::Suite.new(opts) }.must_raise ArgumentError
317
+ proc { Jamie::Suite.new(opts) }.must_raise Jamie::ClientError
318
318
  end
319
319
 
320
320
  it "raises an ArgumentError if run_list is missing" do
321
321
  opts.delete(:run_list)
322
- proc { Jamie::Suite.new(opts) }.must_raise ArgumentError
322
+ proc { Jamie::Suite.new(opts) }.must_raise Jamie::ClientError
323
323
  end
324
324
 
325
325
  it "returns an empty Hash given no attributes" do
@@ -352,7 +352,7 @@ describe Jamie::Platform do
352
352
 
353
353
  it "raises an ArgumentError if name is missing" do
354
354
  opts.delete(:name)
355
- proc { Jamie::Platform.new(opts) }.must_raise ArgumentError
355
+ proc { Jamie::Platform.new(opts) }.must_raise Jamie::ClientError
356
356
  end
357
357
 
358
358
  it "returns an empty Array given no run_list" do
@@ -399,12 +399,12 @@ describe Jamie::Instance do
399
399
 
400
400
  it "raises an ArgumentError if suite is missing" do
401
401
  opts.delete(:suite)
402
- proc { Jamie::Instance.new(opts) }.must_raise ArgumentError
402
+ proc { Jamie::Instance.new(opts) }.must_raise Jamie::ClientError
403
403
  end
404
404
 
405
405
  it "raises an ArgumentError if platform is missing" do
406
406
  opts.delete(:platform)
407
- proc { Jamie::Instance.new(opts) }.must_raise ArgumentError
407
+ proc { Jamie::Instance.new(opts) }.must_raise Jamie::ClientError
408
408
  end
409
409
 
410
410
  it "returns suite" do
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.beta1
4
+ version: 0.1.0.beta2
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-12 00:00:00.000000000 Z
12
+ date: 2013-01-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: celluloid