cluster_chef 3.0.5

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