cluster_chef 3.0.5

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.
Files changed (46) hide show
  1. data/.gitignore +51 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +63 -0
  4. data/Gemfile +18 -0
  5. data/LICENSE +201 -0
  6. data/README.md +332 -0
  7. data/Rakefile +92 -0
  8. data/TODO.md +8 -0
  9. data/VERSION +1 -0
  10. data/chefignore +41 -0
  11. data/cluster_chef.gemspec +115 -0
  12. data/clusters/website_demo.rb +65 -0
  13. data/config/client.rb +59 -0
  14. data/lib/cluster_chef/chef_layer.rb +297 -0
  15. data/lib/cluster_chef/cloud.rb +409 -0
  16. data/lib/cluster_chef/cluster.rb +118 -0
  17. data/lib/cluster_chef/compute.rb +144 -0
  18. data/lib/cluster_chef/cookbook_munger/README.md.erb +47 -0
  19. data/lib/cluster_chef/cookbook_munger/licenses.yaml +16 -0
  20. data/lib/cluster_chef/cookbook_munger/metadata.rb.erb +23 -0
  21. data/lib/cluster_chef/cookbook_munger.rb +588 -0
  22. data/lib/cluster_chef/deprecated.rb +33 -0
  23. data/lib/cluster_chef/discovery.rb +158 -0
  24. data/lib/cluster_chef/dsl_object.rb +123 -0
  25. data/lib/cluster_chef/facet.rb +144 -0
  26. data/lib/cluster_chef/fog_layer.rb +134 -0
  27. data/lib/cluster_chef/private_key.rb +110 -0
  28. data/lib/cluster_chef/role_implications.rb +49 -0
  29. data/lib/cluster_chef/security_group.rb +103 -0
  30. data/lib/cluster_chef/server.rb +265 -0
  31. data/lib/cluster_chef/server_slice.rb +259 -0
  32. data/lib/cluster_chef/volume.rb +93 -0
  33. data/lib/cluster_chef.rb +137 -0
  34. data/notes/aws_console_screenshot.jpg +0 -0
  35. data/rspec.watchr +29 -0
  36. data/spec/cluster_chef/cluster_spec.rb +13 -0
  37. data/spec/cluster_chef/facet_spec.rb +70 -0
  38. data/spec/cluster_chef/server_slice_spec.rb +19 -0
  39. data/spec/cluster_chef/server_spec.rb +112 -0
  40. data/spec/cluster_chef_spec.rb +193 -0
  41. data/spec/spec_helper/dummy_chef.rb +25 -0
  42. data/spec/spec_helper.rb +50 -0
  43. data/spec/test_config.rb +20 -0
  44. data/tasks/chef_config.rb +38 -0
  45. data/tasks/jeweler_use_alt_branch.rb +47 -0
  46. metadata +227 -0
data/Rakefile ADDED
@@ -0,0 +1,92 @@
1
+ #
2
+ # Rakefile for Cluster Chef Knife plugins
3
+ #
4
+ # Author:: Adam Jacob (<adam@opscode.com>)
5
+ # Copyright:: Copyright (c) 2008 Opscode, Inc.
6
+ # License:: Apache License, Version 2.0
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+
21
+ require 'rubygems' unless defined?(Gem)
22
+ require 'bundler'
23
+ begin
24
+ Bundler.setup(:default, :development)
25
+ rescue Bundler::BundlerError => e
26
+ $stderr.puts e.message
27
+ $stderr.puts "Run `bundle install` to install missing gems"
28
+ exit e.status_code
29
+ end
30
+ require 'json'
31
+ require 'jeweler'
32
+ require 'rspec/core/rake_task'
33
+ require 'yard'
34
+
35
+ # Load constants from rake config file.
36
+ Dir[File.join(File.dirname(__FILE__), 'tasks', '*.rb')].sort.each{|f| p f ; require f }
37
+
38
+ $jeweler_push_from_branch = 'version_3'
39
+
40
+ # ---------------------------------------------------------------------------
41
+ #
42
+ # Jeweler -- release cluster_chef as a gem
43
+ #
44
+ Jeweler::Tasks.new do |gem|
45
+ gem.name = ENV['CLUSTER_CHEF_NAME'] || "cluster_chef-knife"
46
+ gem.homepage = "http://infochimps.com/labs"
47
+ gem.license = NEW_COOKBOOK_LICENSE.to_s
48
+ gem.summary = %Q{cluster_chef allows you to orchestrate not just systems but clusters of machines. It includes a powerful layer on top of knife and a collection of cloud cookbooks.}
49
+ gem.description = %Q{cluster_chef allows you to orchestrate not just systems but clusters of machines. It includes a powerful layer on top of knife and a collection of cloud cookbooks.}
50
+ gem.email = SSL_EMAIL_ADDRESS
51
+ gem.authors = ["Infochimps"]
52
+
53
+ ignores = File.readlines(".gitignore").grep(/^[^#]\S+/).map{|s| s.chomp }
54
+ dotfiles = [".gemtest", ".gitignore", ".rspec", ".yardopts"]
55
+ gem.files = dotfiles + Dir["**/*"].
56
+ reject{|f| f =~ %r{^cookbooks/} }.
57
+ reject{|f| File.directory?(f) }.
58
+ reject{|f| ignores.any?{|i| File.fnmatch(i, f) || File.fnmatch(i+'/*', f) || File.fnmatch(i+'/**/*', f) } }
59
+ gem.test_files = gem.files.grep(/^spec\//)
60
+ gem.require_paths = ['lib']
61
+
62
+ if gem.name == 'cluster_chef'
63
+ gem.files.reject!{|f| f =~ %r{^(cluster_chef-knife.gemspec|lib/chef/knife/)} }
64
+ else
65
+ gem.files.reject!{|f| f =~ %r{^(cluster_chef.gemspec|lib/cluster_chef)} }
66
+ end
67
+ end
68
+ Jeweler::RubygemsDotOrgTasks.new
69
+
70
+ # ---------------------------------------------------------------------------
71
+ #
72
+ # RSpec -- testing
73
+ #
74
+ RSpec::Core::RakeTask.new(:spec) do |spec|
75
+ spec.pattern = FileList['spec/**/*_spec.rb']
76
+ end
77
+
78
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
79
+ spec.pattern = 'spec/**/*_spec.rb'
80
+ spec.rcov = true
81
+ spec.rcov_opts = %w[ --exclude .rvm --no-comments --text-summary]
82
+ end
83
+
84
+ # ---------------------------------------------------------------------------
85
+ #
86
+ # Yard -- documentation
87
+ #
88
+ YARD::Rake::YardocTask.new
89
+
90
+ # ---------------------------------------------------------------------------
91
+
92
+ task :default => :spec
data/TODO.md ADDED
@@ -0,0 +1,8 @@
1
+ ### From Nathan
2
+ - syntactic sugar for ```server(0).fullname('blah')```
3
+
4
+ ### Knife commands
5
+
6
+ * knife cluster kick fails if service isn't running
7
+ * make clear directions for installing `cluster_chef` and its initial use.
8
+ * knife cluster launch should fail differently if you give it a facet that doesn't exist
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 3.0.5
data/chefignore ADDED
@@ -0,0 +1,41 @@
1
+ # Put files/directories that should be ignored in this file.
2
+ # Lines that start with '# ' are comments.
3
+
4
+ ## OS
5
+ .DS_Store
6
+ Icon?
7
+ nohup.out
8
+
9
+ ## EDITORS
10
+ \#*
11
+ .#*
12
+ *~
13
+ *.sw[a-z]
14
+ *.bak
15
+ REVISION
16
+ TAGS*
17
+ tmtags
18
+ *_flymake.*
19
+ *_flymake
20
+ *.tmproj
21
+ .project
22
+ .settings
23
+ mkmf.log
24
+
25
+ ## COMPILED
26
+ a.out
27
+ *.o
28
+ *.pyc
29
+ *.so
30
+
31
+ ## OTHER SCM
32
+ */.bzr/*
33
+ */.hg/*
34
+ */.svn/*
35
+
36
+ ## Don't send rspecs up in cookbook
37
+ .watchr
38
+ .rspec
39
+ spec/*
40
+ spec/fixtures/*
41
+
@@ -0,0 +1,115 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "cluster_chef"
8
+ s.version = "3.0.5"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Infochimps"]
12
+ s.date = "2011-12-11"
13
+ s.description = "cluster_chef allows you to orchestrate not just systems but clusters of machines. It includes a powerful layer on top of knife and a collection of cloud cookbooks."
14
+ s.email = "coders@infochimps.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ ".rspec",
22
+ "CHANGELOG.md",
23
+ "Gemfile",
24
+ "LICENSE",
25
+ "README.md",
26
+ "Rakefile",
27
+ "TODO.md",
28
+ "VERSION",
29
+ "chefignore",
30
+ "cluster_chef.gemspec",
31
+ "clusters/website_demo.rb",
32
+ "config/client.rb",
33
+ "lib/cluster_chef.rb",
34
+ "lib/cluster_chef/chef_layer.rb",
35
+ "lib/cluster_chef/cloud.rb",
36
+ "lib/cluster_chef/cluster.rb",
37
+ "lib/cluster_chef/compute.rb",
38
+ "lib/cluster_chef/cookbook_munger.rb",
39
+ "lib/cluster_chef/cookbook_munger/README.md.erb",
40
+ "lib/cluster_chef/cookbook_munger/licenses.yaml",
41
+ "lib/cluster_chef/cookbook_munger/metadata.rb.erb",
42
+ "lib/cluster_chef/deprecated.rb",
43
+ "lib/cluster_chef/discovery.rb",
44
+ "lib/cluster_chef/dsl_object.rb",
45
+ "lib/cluster_chef/facet.rb",
46
+ "lib/cluster_chef/fog_layer.rb",
47
+ "lib/cluster_chef/private_key.rb",
48
+ "lib/cluster_chef/role_implications.rb",
49
+ "lib/cluster_chef/security_group.rb",
50
+ "lib/cluster_chef/server.rb",
51
+ "lib/cluster_chef/server_slice.rb",
52
+ "lib/cluster_chef/volume.rb",
53
+ "notes/aws_console_screenshot.jpg",
54
+ "rspec.watchr",
55
+ "spec/cluster_chef/cluster_spec.rb",
56
+ "spec/cluster_chef/facet_spec.rb",
57
+ "spec/cluster_chef/server_slice_spec.rb",
58
+ "spec/cluster_chef/server_spec.rb",
59
+ "spec/cluster_chef_spec.rb",
60
+ "spec/spec_helper.rb",
61
+ "spec/spec_helper/dummy_chef.rb",
62
+ "spec/test_config.rb",
63
+ "tasks/chef_config.rb",
64
+ "tasks/jeweler_use_alt_branch.rb"
65
+ ]
66
+ s.homepage = "http://infochimps.com/labs"
67
+ s.licenses = ["apachev2"]
68
+ s.require_paths = ["lib"]
69
+ s.rubygems_version = "1.8.11"
70
+ s.summary = "cluster_chef allows you to orchestrate not just systems but clusters of machines. It includes a powerful layer on top of knife and a collection of cloud cookbooks."
71
+ s.test_files = ["spec/cluster_chef/cluster_spec.rb", "spec/cluster_chef/facet_spec.rb", "spec/cluster_chef/server_slice_spec.rb", "spec/cluster_chef/server_spec.rb", "spec/cluster_chef_spec.rb", "spec/spec_helper/dummy_chef.rb", "spec/spec_helper.rb", "spec/test_config.rb"]
72
+
73
+ if s.respond_to? :specification_version then
74
+ s.specification_version = 3
75
+
76
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
77
+ s.add_runtime_dependency(%q<chef>, ["~> 0.10.4"])
78
+ s.add_runtime_dependency(%q<fog>, ["~> 1.1.1"])
79
+ s.add_runtime_dependency(%q<formatador>, ["~> 0.2.1"])
80
+ s.add_runtime_dependency(%q<gorillib>, ["~> 0.1.7"])
81
+ s.add_development_dependency(%q<bundler>, ["~> 1"])
82
+ s.add_development_dependency(%q<yard>, ["~> 0.6.7"])
83
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
84
+ s.add_development_dependency(%q<rspec>, ["~> 2.7.0"])
85
+ s.add_development_dependency(%q<configliere>, ["~> 0.4.8"])
86
+ s.add_development_dependency(%q<spork>, ["~> 0.9.0.rc5"])
87
+ s.add_development_dependency(%q<watchr>, ["~> 0.7"])
88
+ else
89
+ s.add_dependency(%q<chef>, ["~> 0.10.4"])
90
+ s.add_dependency(%q<fog>, ["~> 1.1.1"])
91
+ s.add_dependency(%q<formatador>, ["~> 0.2.1"])
92
+ s.add_dependency(%q<gorillib>, ["~> 0.1.7"])
93
+ s.add_dependency(%q<bundler>, ["~> 1"])
94
+ s.add_dependency(%q<yard>, ["~> 0.6.7"])
95
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
96
+ s.add_dependency(%q<rspec>, ["~> 2.7.0"])
97
+ s.add_dependency(%q<configliere>, ["~> 0.4.8"])
98
+ s.add_dependency(%q<spork>, ["~> 0.9.0.rc5"])
99
+ s.add_dependency(%q<watchr>, ["~> 0.7"])
100
+ end
101
+ else
102
+ s.add_dependency(%q<chef>, ["~> 0.10.4"])
103
+ s.add_dependency(%q<fog>, ["~> 1.1.1"])
104
+ s.add_dependency(%q<formatador>, ["~> 0.2.1"])
105
+ s.add_dependency(%q<gorillib>, ["~> 0.1.7"])
106
+ s.add_dependency(%q<bundler>, ["~> 1"])
107
+ s.add_dependency(%q<yard>, ["~> 0.6.7"])
108
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
109
+ s.add_dependency(%q<rspec>, ["~> 2.7.0"])
110
+ s.add_dependency(%q<configliere>, ["~> 0.4.8"])
111
+ s.add_dependency(%q<spork>, ["~> 0.9.0.rc5"])
112
+ s.add_dependency(%q<watchr>, ["~> 0.7"])
113
+ end
114
+ end
115
+
@@ -0,0 +1,65 @@
1
+ ClusterChef.cluster 'webserver_demo' do
2
+ cloud :ec2 do
3
+ defaults
4
+ availability_zones ['us-east-1d']
5
+ flavor 't1.micro' # change to something larger for serious use
6
+ backing 'ebs'
7
+ image_name 'natty'
8
+ bootstrap_distro 'ubuntu10.04-cluster_chef'
9
+ chef_client_script 'client.rb'
10
+ mount_ephemerals(:tags => { :scratch_dirs => true })
11
+ end
12
+
13
+ role "nfs_client"
14
+ recipe "package_set"
15
+
16
+ facet :webnode do
17
+ instances 6
18
+ role "nginx"
19
+ role "redis_client"
20
+ role "mysql_client"
21
+ role "elasticsearch_client"
22
+ role "awesome_website"
23
+ role "web_server" # this triggers opening appropriate ports
24
+ # Rotate nodes among availability zones
25
+ azs = ['us-east-1d', 'us-east-1b', 'us-east-1c']
26
+ (0...instances).each do |idx|
27
+ server(idx).cloud.availability_zones [azs[ idx % azs.length ]]
28
+ end
29
+ # Rote nodes among A/B testing groups
30
+ (0..instances).each do |idx|
31
+ server(idx).chef_node.normal[:split_testing] = ( (idx % 2 == 0) ? 'A' : 'B' )
32
+ end
33
+ end
34
+
35
+ facet :dbnode do
36
+ instances 2
37
+ role "mysql_server"
38
+ role "redis_client"
39
+ # burly master, wussier slaves
40
+ cloud.flavor "m1.large"
41
+ server(0) do
42
+ cloud.flavor "c1.xlarge"
43
+ end
44
+
45
+ volume(:data) do
46
+ size 50
47
+ keep true
48
+ device '/dev/sdi'
49
+ mount_point '/data/db'
50
+ mount_options 'defaults,nouuid,noatime'
51
+ fstype 'xfs'
52
+ snapshot_id 'snap-d9c1edb1'
53
+ end
54
+ end
55
+
56
+ facet :esnode do
57
+ instances 1
58
+ role "nginx"
59
+ role "redis_server"
60
+ role "elasticsearch_data_esnode"
61
+ role "elasticsearch_http_esnode"
62
+ #
63
+ cloud.flavor "m1.large"
64
+ end
65
+ end
data/config/client.rb ADDED
@@ -0,0 +1,59 @@
1
+ require "ohai"
2
+ require "json"
3
+
4
+ #
5
+ # Load configuration
6
+ #
7
+
8
+ def merge_safely hsh
9
+ hsh.merge!( yield ) rescue Mash.new
10
+ end
11
+
12
+ def create_file_if_empty(filename, str)
13
+ unless File.exists?(filename)
14
+ puts "Populating #{filename}" ;
15
+ File.open(filename, "w", 0600){|f| f.puts(str) }
16
+ end
17
+ end
18
+
19
+ def present?(config, key)
20
+ not config[key].to_s.empty?
21
+ end
22
+
23
+ # Start with a set of defaults
24
+ chef_config = Mash.new
25
+
26
+ # Extract client configuration from EC2 user-data
27
+ OHAI_INFO = Ohai::System.new
28
+ OHAI_INFO.all_plugins
29
+ merge_safely(chef_config){ JSON.parse(OHAI_INFO[:ec2][:userdata]) }
30
+
31
+ #
32
+ # Configure chef run
33
+ #
34
+
35
+ log_level :info
36
+ log_location STDOUT
37
+ node_name chef_config["node_name"] if chef_config["node_name"]
38
+ chef_server_url chef_config["chef_server"] if chef_config["chef_server"]
39
+ validation_client_name chef_config["validation_client_name"] if chef_config["validation_client_name"]
40
+ validation_key "/etc/chef/validation.pem"
41
+ client_key "/etc/chef/client.pem"
42
+ node_attrs_file "/etc/chef/first-boot.json"
43
+
44
+ # If the client file is missing, write the validation key out so chef-client can register
45
+ unless File.exists?(client_key)
46
+ if present?(chef_config, "client_key") then create_file_if_empty(client_key, chef_config["client_key"])
47
+ elsif present?(chef_config, "validation_key") then create_file_if_empty(validation_key, chef_config["validation_key"])
48
+ else warn "Yikes -- I have no client key or validation key!!"
49
+ end
50
+ end
51
+
52
+ reduced_chef_config = chef_config.reject{|k,v| k.to_s =~ /(_key|run_list)$/ }
53
+ unless File.exists?(node_attrs_file)
54
+ create_file_if_empty(node_attrs_file, JSON.pretty_generate(reduced_chef_config))
55
+ end
56
+ json_attribs node_attrs_file
57
+
58
+ Chef::Log.debug(JSON.generate(chef_config))
59
+ Chef::Log.info("=> chef client #{node_name} on #{chef_server_url} in cluster +#{chef_config["cluster_name"]}+")
@@ -0,0 +1,297 @@
1
+ #
2
+ # OK so things get a little fishy here, and it's all Opscode's fault ;-)
3
+ #
4
+ # There's currently no API for setting ACLs. However, if the *client the
5
+ # node will run as* is the *client that creates the node*, it is granted the
6
+ # correct permissions.
7
+ #
8
+ # * client exists, node exists: don't need to do anything. We trust that permissions are correct.
9
+ # * client absent, node exists: client created, node is fine. We trust that permissions are correct.
10
+ # * client absent, node absent: client created, so have key; client creates node, so it has write permissions.
11
+ # * client exists, node absent: FAIL.
12
+ #
13
+ # The current implementation persists the client keys locally to your
14
+ # Chef::Config[:client_key_dir]. This is insecure and unmanageable; and the
15
+ # node will shortly re-register the key, making it invalide anyway.
16
+ #
17
+ # If the client's private_key is empty/wrong and the node is absent, it will
18
+ # cause an error. in that case, you can:
19
+ #
20
+ # * create the node yourself in the management console, and
21
+ # grant access to its eponymous client; OR
22
+ # * nuke the client key from orbit (it's the only way to be sure) and re-run,
23
+ # taking all responsibility for the catastrophic results of an errant nuke; OR
24
+ # * wait for opscode to open API access for ACLs.
25
+ #
26
+ #
27
+
28
+ module ClusterChef
29
+ module DryRunnable
30
+ # Run given block unless in dry_run mode (ClusterChef.chef_config[:dry_run]
31
+ # is true)
32
+ def unless_dry_run
33
+ if ClusterChef.chef_config[:dry_run]
34
+ ui.info(" ... but not really (#{ui.color("dry run", :bold, :yellow)} for server #{name})")
35
+ else
36
+ yield
37
+ end
38
+ end
39
+ end
40
+
41
+ ComputeBuilder.class_eval do
42
+ def new_chef_role(role_name, cluster, facet=nil)
43
+ chef_role = Chef::Role.new
44
+ chef_role.name role_name
45
+ chef_role.description "ClusterChef generated role for #{[cluster_name, facet_name].compact.join('-')}" unless chef_role.description
46
+ chef_role.instance_eval{ @cluster = cluster; @facet = facet; }
47
+ @chef_roles << chef_role
48
+ chef_role
49
+ end
50
+ end
51
+
52
+ ServerSlice.class_eval do
53
+ include DryRunnable
54
+ def sync_roles
55
+ step(" syncing cluster and facet roles")
56
+ unless_dry_run do
57
+ chef_roles.each(&:save)
58
+ end
59
+ end
60
+ end
61
+
62
+ #
63
+ # ClusterChef::Server methods that handle chef actions
64
+ #
65
+ Server.class_eval do
66
+ include DryRunnable
67
+
68
+ # The chef client, if it already exists in the server.
69
+ # Use the 'ensure' method to create/update it.
70
+ def chef_client
71
+ return @chef_client unless @chef_client.nil?
72
+ @chef_client = cluster.find_client(fullname) || false
73
+ end
74
+
75
+ # The chef node, if it already exists in the server.
76
+ # Use the 'ensure' method to create/update it.
77
+ def chef_node
78
+ return @chef_node unless @chef_node.nil?
79
+ @chef_node = cluster.find_node(fullname) || false
80
+ end
81
+
82
+ # true if chef client is created and discovered
83
+ def chef_client?
84
+ chef_client.present?
85
+ end
86
+
87
+ # true if chef node is created and discovered
88
+ def chef_node?
89
+ chef_node.present?
90
+ end
91
+
92
+ def delete_chef
93
+ if chef_node then
94
+ step(" deleting chef node", :red)
95
+ unless_dry_run do
96
+ chef_node.destroy
97
+ end
98
+ @chef_node = nil
99
+ end
100
+ if chef_client
101
+ step(" deleting chef client", :red)
102
+ unless_dry_run do
103
+ chef_client.destroy
104
+ end
105
+ @chef_client = nil
106
+ end
107
+ end
108
+
109
+ # creates or updates the chef node.
110
+ #
111
+ # FIXME: !! this currently doesn't do the right thing for modifications to the
112
+ # chef node. !!
113
+ #
114
+ # See notes at top of file for why all this jiggery-fuckery
115
+ #
116
+ # * client exists, node exists: assume client can update, weep later when
117
+ # the initial chef run fails. Not much we can do here -- holler at opscode.
118
+ # * client exists, node absent: see if client can create, fail otherwise
119
+ # * client absent, node absent: see if client can create both, fail otherwise
120
+ # * client absent, node exists: fail (we can't get permissions)
121
+ def sync_chef_node
122
+ step(" syncing chef node using the server's key")
123
+ # force-fetch the node so that we have its full attributes (the discovery
124
+ # does not pull all of it back)
125
+ @chef_node = handle_chef_response('404'){ Chef::Node.load( fullname ) }
126
+ # sets @chef_client if it exists
127
+ chef_client
128
+ #
129
+ case
130
+ when @chef_client && @chef_node then _update_chef_node # this will fail later if the chef client is in a bad state but whaddayagonnado
131
+ when @chef_client && (! @chef_node) then _create_chef_node
132
+ when (! @chef_client) && (! @chef_node) then # create both
133
+ ensure_chef_client
134
+ _create_chef_node
135
+ when (! @chef_client) && @chef_node
136
+ raise("The #{fullname} node exists, but its client does not.\nDue to limitations in the Opscode API, if we create a client, it will lack write permissions to the node. Small sadness now avoids much sadness later\nYou must either create a client manually, fix its permissions in the Chef console, and drop its client key where we can find it; or (if you are aware of the consequences) do \nknife node delete #{fullname}")
137
+ end
138
+ @chef_node
139
+ end
140
+
141
+ def client_key
142
+ @client_key ||= ClusterChef::ChefClientKey.new("client-#{fullname}", chef_client) do |body|
143
+ chef_client.private_key(body) if chef_client.present? && body.present?
144
+ cloud.user_data(:client_key => body)
145
+ end
146
+ end
147
+
148
+ def chef_client_script_content
149
+ return @chef_client_script_content if @chef_client_script_content
150
+ return unless cloud.chef_client_script
151
+ script_filename = File.expand_path("../../config/#{cloud.chef_client_script}", File.dirname(__FILE__))
152
+ @chef_client_script_content = safely{ File.read(script_filename) }
153
+ end
154
+
155
+ protected
156
+
157
+ # Create the chef client on the server. Do not call this directly -- go
158
+ # through sync_chef_node.
159
+ #
160
+ # this is done as the eponymous client, ensuring that the client does in
161
+ # fact have permissions on the node
162
+ #
163
+ # preconditions: @chef_node is set
164
+ def _create_chef_node(&block)
165
+ step(" creating chef node", :green)
166
+ @chef_node = Chef::Node.new
167
+ @chef_node.name(fullname)
168
+ set_chef_node_attributes
169
+ set_chef_node_environment
170
+ sync_volume_attributes
171
+ unless_dry_run do
172
+ chef_api_server_as_client.post_rest('nodes', @chef_node)
173
+ end
174
+ end
175
+
176
+ # Update the chef client on the server. Do not call this directly -- go
177
+ # through create_or_update_chef_node.
178
+ #
179
+ # this is done as the eponymous client, ensuring that the client does in
180
+ # fact have permissions on the node.
181
+ #
182
+ # preconditions: @chef_node is set
183
+ def _update_chef_node
184
+ step(" updating chef node", :blue)
185
+ set_chef_node_attributes
186
+ set_chef_node_environment
187
+ sync_volume_attributes
188
+ unless_dry_run do
189
+ chef_api_server_as_admin.put_rest("nodes/#{@chef_node.name}", @chef_node)
190
+ end
191
+ end
192
+
193
+
194
+ def sync_volume_attributes
195
+ composite_volumes.each do |vol_name, vol|
196
+ chef_node.normal[:volumes] ||= Mash.new
197
+ chef_node.normal[:volumes][vol_name] = vol.to_mash.compact
198
+ end
199
+ end
200
+
201
+ def set_chef_node_attributes
202
+ step(" setting node runlist and essential attributes")
203
+ @chef_node.run_list = Chef::RunList.new(*@settings[:run_list])
204
+ @chef_node.override[:cluster_name] = cluster_name
205
+ @chef_node.override[:facet_name] = facet_name
206
+ @chef_node.override[:facet_index] = facet_index
207
+ end
208
+
209
+ def set_chef_node_environment
210
+ @chef_node.chef_environment(environment.to_s)
211
+ end
212
+
213
+ #
214
+ # Don't call this directly -- only through ensure_chef_node_and_client
215
+ #
216
+ def ensure_chef_client
217
+ step(" ensuring chef client exists")
218
+ return @chef_client if chef_client
219
+ step( " creating chef client", :green)
220
+ @chef_client = Chef::ApiClient.new
221
+ @chef_client.name(fullname)
222
+ @chef_client.admin(false)
223
+ #
224
+ # ApiClient#create sends extra params that fail -- we'll do it ourselves
225
+ # purposefully *not* catching the 'but it already exists' error: if it
226
+ # didn't show up in the discovery process, we're in an inconsistent state
227
+ unless_dry_run do
228
+ response = chef_api_server_as_admin.post_rest("clients", { 'name' => fullname, 'admin' => false, 'private_key' => true })
229
+ client_key.body = response['private_key']
230
+ end
231
+ client_key.save
232
+ @chef_client
233
+ end
234
+
235
+ def chef_api_server_as_client
236
+ return @chef_api_server_as_client if @chef_api_server_as_client
237
+ unless File.exists?(client_key.filename)
238
+ raise("Cannot create chef node #{fullname} -- client #{@chef_client} exists but no client key found in #{client_key.filename}.")
239
+ end
240
+ @chef_api_server_as_client = Chef::REST.new(Chef::Config[:chef_server_url], fullname, client_key.filename)
241
+ end
242
+
243
+ def chef_api_server_as_admin
244
+ @chef_api_server_as_admin ||= Chef::REST.new(Chef::Config[:chef_server_url])
245
+ end
246
+
247
+ # Execute the given chef call, but don't explode if the given http status
248
+ # code comes back
249
+ #
250
+ # @return chef object, or false if the server returned a recoverable response
251
+ def handle_chef_response(recoverable_responses, &block)
252
+ begin
253
+ block.call
254
+ rescue Net::HTTPServerException => e
255
+ raise unless Array(recoverable_responses).include?(e.response.code)
256
+ Chef::Log.debug("Swallowing a #{e.response.code} response in #{self.fullname}: #{e}")
257
+ return false
258
+ end
259
+ end
260
+
261
+ #
262
+ # The below *was* present but was pulled from the API by opscode for some reason (2011/10/20)
263
+ #
264
+
265
+ # # The client is required to have these permissions on its eponymous node
266
+ # REQUIRED_PERMISSIONS = %w[read create update]
267
+ #
268
+ # #
269
+ # # Verify that the client has required _acl's on the node.
270
+ # #
271
+ # # We don't raise an error, just a very noisy warning.
272
+ # #
273
+ # def check_node_permissions
274
+ # step(" ensuring chef node permissions are correct")
275
+ # chef_server_rest = Chef::REST.new(Chef::Config[:chef_server_url])
276
+ # handle_chef_response('404') do
277
+ # perms = chef_server_rest.get_rest("nodes/#{fullname}/_acl")
278
+ # perms_valid = {}
279
+ # REQUIRED_PERMISSIONS.each{|perm| perms_valid[perm] = perms[perm] && perms[perm]['actors'].include?(fullname) }
280
+ # Chef::Log.debug("Checking permissions: #{perms_valid.inspect} -- #{ perms_valid.values.all? ? 'correct' : 'BADNESS' }")
281
+ # unless perms_valid.values.all?
282
+ # ui.info(" ************************ ")
283
+ # ui.info(" ")
284
+ # ui.info(" INCONSISTENT PERMISSIONS for node #{fullname}:")
285
+ # ui.info(" The client[#{fullname}] should have permissions for #{REQUIRED_PERMISSIONS.join(', ')}")
286
+ # ui.info(" Instead, they are #{perms_valid.inspect}")
287
+ # ui.info(" You should create the node #{fullname} as client[#{fullname}], not as yourself.")
288
+ # ui.info(" ")
289
+ # ui.info(" Please adjust the permissions on the Opscode console, at")
290
+ # ui.info(" https://manage.opscode.com/nodes/#{fullname}/_acl")
291
+ # ui.info(" ")
292
+ # ui.info(" ************************ ")
293
+ # end
294
+ # end
295
+ # end
296
+ end
297
+ end