jamie 0.1.0.beta1 → 0.1.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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