jamie 0.1.0.alpha8 → 0.1.0.alpha9

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/jamie.gemspec CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |gem|
18
18
  gem.require_paths = ["lib"]
19
19
 
20
20
  gem.add_dependency 'thor'
21
+ gem.add_dependency 'net-ssh'
22
+ gem.add_dependency 'net-scp'
21
23
  gem.add_dependency 'mixlib-shellout'
22
24
  gem.add_dependency 'vagrant', '~> 1.0.5'
23
25
 
@@ -9,58 +9,45 @@ module Jamie
9
9
  module Driver
10
10
 
11
11
  # Vagrant driver for Jamie. It communicates to Vagrant via the CLI.
12
- class Vagrant < Jamie::Driver::Base
12
+ class Vagrant < Jamie::Driver::SSHBase
13
13
 
14
14
  default_config 'memory', '256'
15
15
 
16
- def create(instance)
17
- run "vagrant up #{instance.name} --no-provision"
16
+ def perform_create(instance, state)
17
+ state['name'] = instance.name
18
+ run "vagrant up #{state['name']} --no-provision"
18
19
  end
19
20
 
20
- def converge(instance)
21
- run "vagrant provision #{instance.name}"
21
+ def perform_converge(instance, state)
22
+ run "vagrant provision #{state['name']}"
22
23
  end
23
24
 
24
- def setup(instance)
25
- if instance.jr.setup_cmd
26
- ssh instance, instance.jr.setup_cmd
27
- else
28
- super
29
- end
25
+ def perform_destroy(instance, state)
26
+ run "vagrant destroy #{state['name']} -f"
27
+ state.delete('name')
30
28
  end
31
29
 
32
- def verify(instance)
33
- if instance.jr.run_cmd
34
- ssh instance, instance.jr.sync_cmd
35
- ssh instance, instance.jr.run_cmd
36
- else
37
- super
38
- end
39
- end
30
+ protected
40
31
 
41
- def destroy(instance)
42
- run "vagrant destroy #{instance.name} -f"
32
+ def generate_ssh_args(state)
33
+ Array(state['name'])
43
34
  end
44
35
 
45
- private
36
+ def ssh(ssh_args, cmd)
37
+ run %{vagrant ssh #{ssh_args.first} --command '#{cmd}'}
38
+ end
46
39
 
47
40
  def run(cmd)
48
41
  puts " [vagrant command] '#{display_cmd(cmd)}'"
49
- shellout = Mixlib::ShellOut.new(
50
- cmd, :live_stream => STDOUT, :timeout => 60000
51
- )
52
- shellout.run_command
53
- puts " [vagrant command] '#{display_cmd(cmd)}' ran " +
54
- "in #{shellout.execution_time} seconds."
55
- shellout.error!
42
+ sh = Mixlib::ShellOut.new(cmd, :live_stream => STDOUT,
43
+ :timeout => 60000)
44
+ sh.run_command
45
+ puts " [vagrant command] ran in #{sh.execution_time} seconds."
46
+ sh.error!
56
47
  rescue Mixlib::ShellOut::ShellCommandFailed => ex
57
48
  raise ActionFailed, ex.message
58
49
  end
59
50
 
60
- def ssh(instance, cmd)
61
- run %{vagrant ssh #{instance.name} --command '#{cmd}'}
62
- end
63
-
64
51
  def display_cmd(cmd)
65
52
  parts = cmd.partition("\n")
66
53
  parts[1] == "\n" ? "#{parts[0]}..." : cmd
data/lib/jamie/vagrant.rb CHANGED
@@ -45,25 +45,11 @@ module Jamie
45
45
  c.vm.provision :chef_solo do |chef|
46
46
  chef.log_level = config.jamie.log_level
47
47
  chef.run_list = instance.run_list
48
- chef.json = instance.json
49
- chef.data_bags_path = calculate_data_bags_path(config, instance)
48
+ chef.json = instance.attributes
49
+ chef.data_bags_path = instance.suite.data_bags_path
50
50
  end
51
51
  end
52
52
  end
53
-
54
- def self.calculate_data_bags_path(config, instance)
55
- base_path = config.jamie.test_base_path
56
- instance_data_bags_path = File.join(base_path, instance.name, "data_bags")
57
- common_data_bags_path = File.join(base_path, "data_bags")
58
-
59
- if File.directory?(instance_data_bags_path)
60
- instance_data_bags_path
61
- elsif File.directory?(common_data_bags_path)
62
- common_data_bags_path
63
- else
64
- nil
65
- end
66
- end
67
53
  end
68
54
  end
69
55
 
data/lib/jamie/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Jamie
4
4
 
5
- VERSION = "0.1.0.alpha8"
5
+ VERSION = "0.1.0.alpha9"
6
6
  end
data/lib/jamie.rb CHANGED
@@ -3,7 +3,14 @@
3
3
  require 'base64'
4
4
  require 'delegate'
5
5
  require 'digest'
6
+ require 'fileutils'
7
+ require 'json'
8
+ require 'mixlib/shellout'
6
9
  require 'net/https'
10
+ require 'net/scp'
11
+ require 'net/ssh'
12
+ require 'socket'
13
+ require 'stringio'
7
14
  require 'yaml'
8
15
  require 'vendor/hash_recursive_merge'
9
16
 
@@ -51,7 +58,7 @@ module Jamie
51
58
  # convergence integration
52
59
  def suites
53
60
  @suites ||= Collection.new(
54
- Array(yaml["suites"]).map { |hash| Suite.new(hash) })
61
+ Array(yaml["suites"]).map { |hash| new_suite(hash) })
55
62
  end
56
63
 
57
64
  # @return [Array<Instance>] all instances, resulting from all platform and
@@ -113,8 +120,14 @@ module Jamie
113
120
 
114
121
  private
115
122
 
123
+ def new_suite(hash)
124
+ data_bags_path = calculate_data_bags_path(hash['name'])
125
+ Suite.new(hash.rmerge({ 'data_bags_path' => data_bags_path }))
126
+ end
127
+
116
128
  def new_platform(hash)
117
129
  mpc = merge_platform_config(hash)
130
+ mpc['driver_config']['jamie_root'] = File.dirname(yaml_file)
118
131
  mpc['driver'] = new_driver(mpc['driver_plugin'], mpc['driver_config'])
119
132
  Platform.new(mpc)
120
133
  end
@@ -146,6 +159,19 @@ module Jamie
146
159
  default_driver_config.rmerge(common_driver_config.rmerge(platform_config))
147
160
  end
148
161
 
162
+ def calculate_data_bags_path(suite_name)
163
+ suite_data_bags_path = File.join(test_base_path, suite_name, "data_bags")
164
+ common_data_bags_path = File.join(test_base_path, "data_bags")
165
+
166
+ if File.directory?(suite_data_bags_path)
167
+ suite_data_bags_path
168
+ elsif File.directory?(common_data_bags_path)
169
+ common_data_bags_path
170
+ else
171
+ nil
172
+ end
173
+ end
174
+
149
175
  def default_driver_config
150
176
  { 'driver_plugin' => DEFAULT_DRIVER_PLUGIN }
151
177
  end
@@ -166,7 +192,11 @@ module Jamie
166
192
  attr_reader :run_list
167
193
 
168
194
  # @return [Hash] Hash of Chef node attributes
169
- attr_reader :json
195
+ attr_reader :attributes
196
+
197
+ # @return [String] local path to the suite's data bags, or nil if one does
198
+ # not exist
199
+ attr_reader :data_bags_path
170
200
 
171
201
  # Constructs a new suite.
172
202
  #
@@ -174,13 +204,15 @@ module Jamie
174
204
  # @option options [String] :name logical name of this suit (**Required**)
175
205
  # @option options [String] :run_list Array of Chef run_list items
176
206
  # (**Required**)
177
- # @option options [Hash] :json Hash of Chef node attributes
207
+ # @option options [Hash] :attributes Hash of Chef node attributes
208
+ # @option options [String] :data_bags_path path to data bags
178
209
  def initialize(options = {})
179
210
  validate_options(options)
180
211
 
181
212
  @name = options['name']
182
213
  @run_list = options['run_list']
183
- @json = options['json'] || Hash.new
214
+ @attributes = options['attributes'] || Hash.new
215
+ @data_bags_path = options['data_bags_path']
184
216
  end
185
217
 
186
218
  private
@@ -208,7 +240,7 @@ module Jamie
208
240
  attr_reader :run_list
209
241
 
210
242
  # @return [Hash] Hash of Chef node attributes
211
- attr_reader :json
243
+ attr_reader :attributes
212
244
 
213
245
  # Constructs a new platform.
214
246
  #
@@ -219,14 +251,14 @@ module Jamie
219
251
  # will manage this platform's lifecycle actions (**Required**)
220
252
  # @option options [Array<String>] :run_list Array of Chef run_list
221
253
  # items
222
- # @option options [Hash] :json Hash of Chef node attributes
254
+ # @option options [Hash] :attributes Hash of Chef node attributes
223
255
  def initialize(options = {})
224
256
  validate_options(options)
225
257
 
226
258
  @name = options['name']
227
259
  @driver = options['driver']
228
260
  @run_list = Array(options['run_list'])
229
- @json = options['json'] || Hash.new
261
+ @attributes = options['attributes'] || Hash.new
230
262
  end
231
263
 
232
264
  private
@@ -279,8 +311,12 @@ module Jamie
279
311
  # suite overriding values from the platform.
280
312
  #
281
313
  # @return [Hash] merged hash of Chef node attributes
282
- def json
283
- platform.json.rmerge(suite.json)
314
+ def attributes
315
+ platform.attributes.rmerge(suite.attributes)
316
+ end
317
+
318
+ def dna
319
+ attributes.rmerge({ 'run_list' => run_list })
284
320
  end
285
321
 
286
322
  # Creates this instance.
@@ -552,40 +588,90 @@ module Jamie
552
588
  # @param attr [Object] configuration key
553
589
  # @return [Object] value at configuration key
554
590
  def [](attr)
555
- @config[attr]
591
+ config[attr]
556
592
  end
557
593
 
558
594
  # Creates an instance.
559
595
  #
560
596
  # @param instance [Instance] an instance
561
597
  # @raise [ActionFailed] if the action could not be completed
562
- def create(instance) ; end
598
+ def create(instance)
599
+ action(:create, instance)
600
+ end
563
601
 
564
602
  # Converges a running instance.
565
603
  #
566
604
  # @param instance [Instance] an instance
567
605
  # @raise [ActionFailed] if the action could not be completed
568
- def converge(instance) ; end
606
+ def converge(instance)
607
+ action(:converge, instance)
608
+ end
569
609
 
570
610
  # Sets up an instance.
571
611
  #
572
612
  # @param instance [Instance] an instance
573
613
  # @raise [ActionFailed] if the action could not be completed
574
- def setup(instance) ; end
614
+ def setup(instance)
615
+ action(:setup, instance)
616
+ end
575
617
 
576
618
  # Verifies a converged instance.
577
619
  #
578
620
  # @param instance [Instance] an instance
579
621
  # @raise [ActionFailed] if the action could not be completed
580
- def verify(instance) ; end
622
+ def verify(instance)
623
+ action(:verify, instance)
624
+ end
581
625
 
582
626
  # Destroys an instance.
583
627
  #
584
628
  # @param instance [Instance] an instance
585
629
  # @raise [ActionFailed] if the action could not be completed
586
- def destroy(instance) ; end
630
+ def destroy(instance)
631
+ action(:destroy, instance)
632
+ destroy_state(instance)
633
+ end
634
+
635
+ protected
636
+
637
+ attr_reader :config
638
+
639
+ def action(what, instance)
640
+ state = load_state(instance)
641
+ public_send("perform_#{what}", instance, state)
642
+ state['last_action'] = what.to_s
643
+ ensure
644
+ dump_state(instance, state)
645
+ end
646
+
647
+ def load_state(instance)
648
+ statefile = state_filepath(instance)
587
649
 
588
- private
650
+ if File.exists?(statefile)
651
+ YAML.load_file(statefile)
652
+ else
653
+ { 'name' => instance.name }
654
+ end
655
+ end
656
+
657
+ def dump_state(instance, state)
658
+ statefile = state_filepath(instance)
659
+ dir = File.dirname(statefile)
660
+
661
+ FileUtils.mkdir_p(dir) if !File.directory?(dir)
662
+ File.open(statefile, "wb") { |f| f.write(YAML.dump(state)) }
663
+ end
664
+
665
+ def destroy_state(instance)
666
+ statefile = state_filepath(instance)
667
+ FileUtils.rm(statefile) if File.exists?(statefile)
668
+ end
669
+
670
+ def state_filepath(instance)
671
+ File.expand_path(File.join(
672
+ config['jamie_root'], ".jamie", "#{instance.name}.yml"
673
+ ))
674
+ end
589
675
 
590
676
  def self.defaults
591
677
  @defaults ||= Hash.new
@@ -595,5 +681,244 @@ module Jamie
595
681
  defaults[attr] = value
596
682
  end
597
683
  end
684
+
685
+ # Base class for a driver that uses SSH to communication with an instance.
686
+ # A subclass must implement the following methods:
687
+ # * #perform_create(instance, state)
688
+ # * #perform_destroy(instance, state)
689
+ class SSHBase < Base
690
+
691
+ def perform_converge(instance, state)
692
+ ssh_args = generate_ssh_args(state)
693
+
694
+ install_omnibus(ssh_args) if config['require_chef_omnibus']
695
+ prepare_chef_home(ssh_args)
696
+ upload_chef_data(ssh_args, instance)
697
+ run_chef_solo(ssh_args)
698
+ end
699
+
700
+ def perform_setup(instance, state)
701
+ ssh_args = generate_ssh_args(state)
702
+
703
+ if instance.jr.setup_cmd
704
+ ssh(ssh_args, instance.jr.setup_cmd)
705
+ else
706
+ super
707
+ end
708
+ end
709
+
710
+ def perform_verify(instance, state)
711
+ ssh_args = generate_ssh_args(state)
712
+
713
+ if instance.jr.run_cmd
714
+ ssh(ssh_args, instance.jr.sync_cmd)
715
+ ssh(ssh_args, instance.jr.run_cmd)
716
+ else
717
+ super
718
+ end
719
+ end
720
+
721
+ protected
722
+
723
+ def generate_ssh_args(state)
724
+ [ state['hostname'],
725
+ config['username'],
726
+ { :password => config['password'] }
727
+ ]
728
+ end
729
+
730
+ def chef_home
731
+ "/tmp/jamie-chef-solo".freeze
732
+ end
733
+
734
+ def install_omnibus(ssh_args)
735
+ ssh(ssh_args, <<-INSTALL)
736
+ if [ ! -d "/opt/chef" ] ; then
737
+ curl -L https://www.opscode.com/chef/install.sh | sudo bash
738
+ fi
739
+ INSTALL
740
+ end
741
+
742
+ def prepare_chef_home(ssh_args)
743
+ ssh(ssh_args, "sudo rm -rf #{chef_home} && mkdir -p #{chef_home}")
744
+ end
745
+
746
+ def upload_chef_data(ssh_args, instance)
747
+ Jamie::ChefDataUploader.new(
748
+ instance, ssh_args, config['jamie_root'], chef_home
749
+ ).upload
750
+ end
751
+
752
+ def run_chef_solo(ssh_args)
753
+ ssh(ssh_args, <<-RUN_SOLO)
754
+ sudo chef-solo -c #{chef_home}/solo.rb -j #{chef_home}/dna.json
755
+ RUN_SOLO
756
+ end
757
+
758
+ def ssh(ssh_args, cmd)
759
+ Net::SSH.start(*ssh_args) do |ssh|
760
+ exit_code = ssh_exec_with_exit!(ssh, cmd)
761
+
762
+ if exit_code != 0
763
+ shorter_cmd = cmd.squeeze(" ").strip
764
+ raise ActionFailed,
765
+ "SSH exited (#{exit_code}) for command: [#{shorter_cmd}]"
766
+ end
767
+ end
768
+ rescue Net::SSH::Exception => ex
769
+ raise ActionFailed, ex.message
770
+ end
771
+
772
+ def ssh_exec_with_exit!(ssh, cmd)
773
+ exit_code = nil
774
+ ssh.open_channel do |channel|
775
+ channel.exec(cmd) do |ch, success|
776
+
777
+ channel.on_data do |ch, data|
778
+ $stdout.print data
779
+ end
780
+
781
+ channel.on_extended_data do |ch, type, data|
782
+ $stderr.print data
783
+ end
784
+
785
+ channel.on_request("exit-status") do |ch, data|
786
+ exit_code = data.read_long
787
+ end
788
+ end
789
+ end
790
+ ssh.loop
791
+ exit_code
792
+ end
793
+
794
+ def wait_for_sshd(hostname)
795
+ print "." until test_ssh(hostname)
796
+ end
797
+
798
+ def test_ssh(hostname)
799
+ socket = TCPSocket.new(hostname, config['port'])
800
+ IO.select([socket], nil, nil, 5)
801
+ rescue SocketError, Errno::ECONNREFUSED,
802
+ Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
803
+ sleep 2
804
+ false
805
+ rescue Errno::EPERM, Errno::ETIMEDOUT
806
+ false
807
+ ensure
808
+ socket && socket.close
809
+ end
810
+ end
811
+ end
812
+
813
+ # Uploads Chef asset files such as dna.json, data bags, and cookbooks to an
814
+ # instance over SSH.
815
+ class ChefDataUploader
816
+
817
+ def initialize(instance, ssh_args, jamie_root, chef_home)
818
+ @instance = instance
819
+ @ssh_args = ssh_args
820
+ @jamie_root = jamie_root
821
+ @chef_home = chef_home
822
+ end
823
+
824
+ def upload
825
+ Net::SCP.start(*ssh_args) do |scp|
826
+ upload_json scp
827
+ upload_solo_rb scp
828
+ upload_cookbooks scp
829
+ upload_data_bags scp if instance.suite.data_bags_path
830
+ end
831
+ end
832
+
833
+ private
834
+
835
+ attr_reader :instance, :ssh_args, :jamie_root, :chef_home
836
+
837
+ def upload_json(scp)
838
+ json_file = StringIO.new(instance.dna.to_json)
839
+ scp.upload!(json_file, "#{chef_home}/dna.json")
840
+ end
841
+
842
+ def upload_solo_rb(scp)
843
+ solo_rb_file = StringIO.new(solo_rb_contents)
844
+ scp.upload!(solo_rb_file, "#{chef_home}/solo.rb")
845
+ end
846
+
847
+ def upload_cookbooks(scp)
848
+ cookbooks_dir = local_cookbooks
849
+ scp.upload!(cookbooks_dir, "#{chef_home}/cookbooks",
850
+ :recursive => true
851
+ ) do |ch, name, sent, total|
852
+ file = name.sub(%r{^#{cookbooks_dir}/}, '')
853
+ puts " #{file}: #{sent}/#{total}"
854
+ end
855
+ ensure
856
+ FileUtils.rmtree(cookbooks_dir)
857
+ end
858
+
859
+ def upload_data_bags(scp)
860
+ data_bags_dir = instance.suite.data_bags_path
861
+ scp.upload!(data_bags_dir, "#{chef_home}/data_bags",
862
+ :recursive => true
863
+ ) do |ch, name, sent, total|
864
+ file = name.sub(%r{^#{data_bags_dir}/}, '')
865
+ puts " #{file}: #{sent}/#{total}"
866
+ end
867
+ end
868
+
869
+ def solo_rb_contents
870
+ solo = []
871
+ solo << %{node_name "#{instance.name}"}
872
+ solo << %{file_cache_path "#{chef_home}/cache"}
873
+ solo << %{cookbook_path "#{chef_home}/cookbooks"}
874
+ solo << %{role_path "#{chef_home}/roles"}
875
+ if instance.suite.data_bags_path
876
+ solo << %{data_bag_path "#{chef_home}/data_bags"}
877
+ end
878
+ solo << %{log_level :info}
879
+ solo.join("\n")
880
+ end
881
+
882
+ def local_cookbooks
883
+ if File.exists?(File.join(jamie_root, "Berksfile"))
884
+ tmpdir = Dir.mktmpdir(instance.name)
885
+ run_berks(tmpdir)
886
+ tmpdir
887
+ elsif File.exists?(File.join(jamie_root, "Cheffile"))
888
+ tmpdir = Dir.mktmpdir(instance.name)
889
+ run_librarian(tmpdir)
890
+ tmpdir
891
+ else
892
+ abort "Berksfile or Cheffile must exist in #{jamie_root}"
893
+ end
894
+ end
895
+
896
+ def run_berks(tmpdir)
897
+ begin
898
+ run "if ! command -v berks >/dev/null ; then exit 1 ; fi"
899
+ rescue Mixlib::ShellOut::ShellCommandFailed
900
+ abort ">>>>>> Berkshelf must be installed, add it to your Gemfile."
901
+ end
902
+ run "berks install --path #{tmpdir}"
903
+ end
904
+
905
+ def run_librarian(tmpdir)
906
+ begin
907
+ run "if ! command -v librarian-chef >/dev/null ; then exit 1 ; fi"
908
+ rescue Mixlib::ShellOut::ShellCommandFailed
909
+ abort ">>>>>> Librarian must be installed, add it to your Gemfile."
910
+ end
911
+ run "librarian-chef install --path #{tmpdir}"
912
+ end
913
+
914
+ def run(cmd)
915
+ puts " [local command] '#{cmd}'"
916
+ sh = Mixlib::ShellOut.new(cmd, :live_stream => STDOUT)
917
+ sh.run_command
918
+ puts " [local command] ran in #{sh.execution_time} seconds."
919
+ sh.error!
920
+ rescue Mixlib::ShellOut::ShellCommandFailed => ex
921
+ raise ActionFailed, ex.message
922
+ end
598
923
  end
599
924
  end
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.alpha8
4
+ version: 0.1.0.alpha9
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: 2012-12-17 00:00:00.000000000 Z
12
+ date: 2012-12-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -27,6 +27,38 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: net-ssh
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: net-scp
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
30
62
  - !ruby/object:Gem::Dependency
31
63
  name: mixlib-shellout
32
64
  requirement: !ruby/object:Gem::Requirement
@@ -159,6 +191,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
159
191
  - - ! '>='
160
192
  - !ruby/object:Gem::Version
161
193
  version: '0'
194
+ segments:
195
+ - 0
196
+ hash: -3537010149917719501
162
197
  required_rubygems_version: !ruby/object:Gem::Requirement
163
198
  none: false
164
199
  requirements: