jamie 0.1.0.alpha8 → 0.1.0.alpha9

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