chef 11.10.0.alpha.1 → 11.10.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/README.md +57 -36
  2. data/distro/common/html/chef-client.8.html +4 -4
  3. data/distro/common/html/chef-expander.8.html +4 -4
  4. data/distro/common/html/chef-expanderctl.8.html +4 -4
  5. data/distro/common/html/chef-server-webui.8.html +4 -4
  6. data/distro/common/html/chef-server.8.html +4 -4
  7. data/distro/common/html/chef-shell.1.html +4 -4
  8. data/distro/common/html/chef-solo.8.html +4 -4
  9. data/distro/common/html/chef-solr.8.html +5 -5
  10. data/distro/common/html/knife-bootstrap.1.html +4 -4
  11. data/distro/common/html/knife-client.1.html +4 -4
  12. data/distro/common/html/knife-configure.1.html +4 -4
  13. data/distro/common/html/knife-cookbook-site.1.html +4 -4
  14. data/distro/common/html/knife-cookbook.1.html +4 -4
  15. data/distro/common/html/knife-data-bag.1.html +4 -4
  16. data/distro/common/html/knife-environment.1.html +4 -4
  17. data/distro/common/html/knife-exec.1.html +4 -4
  18. data/distro/common/html/knife-index.1.html +4 -4
  19. data/distro/common/html/knife-node.1.html +4 -4
  20. data/distro/common/html/knife-role.1.html +4 -4
  21. data/distro/common/html/knife-search.1.html +4 -4
  22. data/distro/common/html/knife-ssh.1.html +4 -4
  23. data/distro/common/html/knife-status.1.html +4 -4
  24. data/distro/common/html/knife-tag.1.html +4 -4
  25. data/distro/common/html/knife.1.html +4 -4
  26. data/distro/common/man/man1/knife-bootstrap.1 +58 -64
  27. data/distro/common/man/man1/knife-client.1 +19 -22
  28. data/distro/common/man/man1/knife-configure.1 +37 -46
  29. data/distro/common/man/man1/knife-cookbook-site.1 +14 -17
  30. data/distro/common/man/man1/knife-cookbook.1 +15 -18
  31. data/distro/common/man/man1/knife-data-bag.1 +14 -17
  32. data/distro/common/man/man1/knife-delete.1 +38 -47
  33. data/distro/common/man/man1/knife-deps.1 +39 -48
  34. data/distro/common/man/man1/knife-diff.1 +43 -52
  35. data/distro/common/man/man1/knife-download.1 +47 -53
  36. data/distro/common/man/man1/knife-edit.1 +32 -41
  37. data/distro/common/man/man1/knife-environment.1 +14 -17
  38. data/distro/common/man/man1/knife-exec.1 +52 -61
  39. data/distro/common/man/man1/knife-index-rebuild.1 +1 -61
  40. data/distro/common/man/man1/knife-list.1 +47 -59
  41. data/distro/common/man/man1/knife-node.1 +15 -18
  42. data/distro/common/man/man1/knife-raw.1 +28 -46
  43. data/distro/common/man/man1/knife-recipe-list.1 +1 -61
  44. data/distro/common/man/man1/knife-role.1 +19 -25
  45. data/distro/common/man/man1/knife-search.1 +53 -62
  46. data/distro/common/man/man1/knife-show.1 +36 -28
  47. data/distro/common/man/man1/knife-ssh.1 +55 -61
  48. data/distro/common/man/man1/knife-status.1 +34 -43
  49. data/distro/common/man/man1/knife-tag.1 +14 -17
  50. data/distro/common/man/man1/knife-upload.1 +47 -56
  51. data/distro/common/man/man1/knife-user.1 +17 -20
  52. data/distro/common/man/man1/knife-xargs.1 +60 -69
  53. data/lib/chef/application.rb +3 -1
  54. data/lib/chef/application/windows_service.rb +0 -1
  55. data/lib/chef/client.rb +41 -152
  56. data/lib/chef/config.rb +19 -23
  57. data/lib/chef/data_bag.rb +1 -1
  58. data/lib/chef/data_bag_item.rb +1 -1
  59. data/lib/chef/exceptions.rb +8 -0
  60. data/lib/chef/formatters/doc.rb +15 -0
  61. data/lib/chef/formatters/error_inspectors/api_error_formatting.rb +2 -1
  62. data/lib/chef/http.rb +18 -8
  63. data/lib/chef/http/authenticator.rb +4 -0
  64. data/lib/chef/http/cookie_manager.rb +3 -0
  65. data/lib/chef/http/decompressor.rb +4 -0
  66. data/lib/chef/http/json_input.rb +4 -0
  67. data/lib/chef/http/json_output.rb +4 -0
  68. data/lib/chef/http/validate_content_length.rb +94 -0
  69. data/lib/chef/knife.rb +0 -1
  70. data/lib/chef/knife/configure.rb +6 -6
  71. data/lib/chef/knife/cookbook_create.rb +2 -2
  72. data/lib/chef/knife/core/subcommand_loader.rb +49 -3
  73. data/lib/chef/knife/ssh.rb +34 -4
  74. data/lib/chef/mixin/path_sanity.rb +1 -0
  75. data/lib/chef/monologger.rb +1 -2
  76. data/lib/chef/node.rb +7 -0
  77. data/lib/chef/policy_builder.rb +49 -0
  78. data/lib/chef/policy_builder/expand_node_object.rb +230 -0
  79. data/lib/chef/policy_builder/policyfile.rb +338 -0
  80. data/lib/chef/provider/file.rb +15 -5
  81. data/lib/chef/provider/group.rb +6 -2
  82. data/lib/chef/provider/group/windows.rb +12 -2
  83. data/lib/chef/provider/http_request.rb +3 -2
  84. data/lib/chef/provider/package.rb +1 -0
  85. data/lib/chef/provider/package/aix.rb +1 -1
  86. data/lib/chef/provider/service/debian.rb +7 -2
  87. data/lib/chef/resource/file.rb +8 -1
  88. data/lib/chef/resource/package.rb +9 -0
  89. data/lib/chef/resource/service.rb +0 -1
  90. data/lib/chef/rest.rb +2 -0
  91. data/lib/chef/run_context.rb +1 -1
  92. data/lib/chef/util/file_edit.rb +1 -1
  93. data/lib/chef/util/windows/net_group.rb +7 -6
  94. data/lib/chef/version.rb +1 -1
  95. data/lib/chef/win32/version.rb +31 -18
  96. data/spec/data/cookbooks/preseed/templates/default/preseed-template-variables.seed +1 -0
  97. data/spec/functional/resource/file_spec.rb +0 -1
  98. data/spec/functional/resource/group_spec.rb +96 -16
  99. data/spec/functional/resource/package_spec.rb +17 -0
  100. data/spec/functional/resource/user_spec.rb +2 -2
  101. data/spec/functional/win32/versions_spec.rb +39 -0
  102. data/spec/integration/client/client_spec.rb +27 -28
  103. data/spec/spec_helper.rb +2 -0
  104. data/spec/support/platform_helpers.rb +7 -1
  105. data/spec/support/shared/functional/file_resource.rb +83 -43
  106. data/spec/unit/application_spec.rb +7 -5
  107. data/spec/unit/client_spec.rb +10 -3
  108. data/spec/unit/config_spec.rb +0 -30
  109. data/spec/unit/cookbook_spec.rb +1 -0
  110. data/spec/unit/data_bag_item_spec.rb +8 -0
  111. data/spec/unit/data_bag_spec.rb +6 -0
  112. data/spec/unit/http_spec.rb +48 -0
  113. data/spec/unit/knife/core/subcommand_loader_spec.rb +77 -1
  114. data/spec/unit/knife/ssh_spec.rb +107 -0
  115. data/spec/unit/mixin/path_sanity_spec.rb +6 -0
  116. data/spec/unit/mixin/securable_spec.rb +77 -3
  117. data/spec/unit/monologger_spec.rb +45 -0
  118. data/spec/unit/node_spec.rb +16 -0
  119. data/spec/unit/policy_builder/expand_node_object_spec.rb +320 -0
  120. data/spec/unit/policy_builder/policyfile_spec.rb +399 -0
  121. data/spec/unit/policy_builder_spec.rb +26 -0
  122. data/spec/unit/provider/deploy_spec.rb +3 -0
  123. data/spec/unit/provider/group/windows_spec.rb +1 -0
  124. data/spec/unit/provider/http_request_spec.rb +23 -1
  125. data/spec/unit/provider/service/debian_service_spec.rb +50 -19
  126. data/spec/unit/recipe_spec.rb +4 -0
  127. data/spec/unit/resource/package_spec.rb +5 -0
  128. data/spec/unit/rest_spec.rb +375 -278
  129. data/spec/unit/run_context_spec.rb +4 -0
  130. metadata +96 -59
  131. checksums.yaml +0 -7
@@ -64,10 +64,14 @@ class Chef
64
64
  :long => "--ssh-user USERNAME",
65
65
  :description => "The ssh username"
66
66
 
67
- option :ssh_password,
68
- :short => "-P PASSWORD",
69
- :long => "--ssh-password PASSWORD",
70
- :description => "The ssh password"
67
+ option :ssh_password_ng,
68
+ :short => "-P [PASSWORD]",
69
+ :long => "--ssh-password [PASSWORD]",
70
+ :description => "The ssh password - will prompt if flag is specified but no password is given",
71
+ # default to a value that can not be a password (boolean)
72
+ # so we can effectively test if this parameter was specified
73
+ # without a vlaue
74
+ :default => false
71
75
 
72
76
  option :ssh_port,
73
77
  :short => "-p PORT",
@@ -432,6 +436,31 @@ class Chef
432
436
  Chef::Config[:knife][:ssh_user])
433
437
  end
434
438
 
439
+ # This is a bit overly complicated because of the way we want knife ssh to work with -P causing a password prompt for
440
+ # the user, but we have to be conscious that this code gets included in knife bootstrap and knife * server create as
441
+ # well. We want to change the semantics so that the default is false and 'nil' means -P without an argument on the
442
+ # command line. But the other utilities expect nil to be the default and we can't prompt in that case. So we effectively
443
+ # use ssh_password_ng to determine if we're coming from knife ssh or from the other utilities. The other utilties can
444
+ # also be patched to use ssh_password_ng easily as long they follow the convention that the default is false.
445
+ def configure_password
446
+ if config.has_key?(:ssh_password_ng) && config[:ssh_password_ng].nil?
447
+ # If the parameter is called on the command line with no value
448
+ # it will set :ssh_password_ng = nil
449
+ # This is where we want to trigger a prompt for password
450
+ config[:ssh_password] = get_password
451
+ else
452
+ # if ssh_password_ng is false then it has not been set at all, and we may be in knife ec2 and still
453
+ # using an old config[:ssh_password]. this is backwards compatibility. all knife cloud plugins should
454
+ # be updated to use ssh_password_ng with a default of false and ssh_password should be retired, (but
455
+ # we'll still need to use the ssh_password out of knife.rb if we find that).
456
+ ssh_password = config.has_key?(:ssh_password_ng) ? config[:ssh_password_ng] : config[:ssh_password]
457
+ # Otherwise, the password has either been specified on the command line,
458
+ # in knife.rb, or key based auth will be attempted
459
+ config[:ssh_password] = get_stripped_unfrozen_value(ssh_password ||
460
+ Chef::Config[:knife][:ssh_password])
461
+ end
462
+ end
463
+
435
464
  def configure_identity_file
436
465
  config[:identity_file] = get_stripped_unfrozen_value(config[:identity_file] ||
437
466
  Chef::Config[:knife][:ssh_identity_file])
@@ -448,6 +477,7 @@ class Chef
448
477
 
449
478
  configure_attribute
450
479
  configure_user
480
+ configure_password
451
481
  configure_identity_file
452
482
  configure_gateway
453
483
  configure_session
@@ -22,6 +22,7 @@ class Chef
22
22
 
23
23
  def enforce_path_sanity(env=ENV)
24
24
  if Chef::Config[:enforce_path_sanity]
25
+ env["PATH"] = "" if env["PATH"].nil?
25
26
  path_separator = Chef::Platform.windows? ? ';' : ':'
26
27
  existing_paths = env["PATH"].split(path_separator)
27
28
  # ensure the Ruby and Gem bindirs are included
@@ -48,9 +48,9 @@ class MonoLogger < Logger
48
48
  @dev = log
49
49
  else
50
50
  @dev = open_logfile(log)
51
- @dev.sync = true
52
51
  @filename = log
53
52
  end
53
+ @dev.sync = true
54
54
  end
55
55
 
56
56
  def write(message)
@@ -75,7 +75,6 @@ class MonoLogger < Logger
75
75
 
76
76
  def create_logfile(filename)
77
77
  logdev = open(filename, (File::WRONLY | File::APPEND | File::CREAT))
78
- logdev.sync = true
79
78
  add_log_header(logdev)
80
79
  logdev
81
80
  end
@@ -247,6 +247,13 @@ class Chef
247
247
  run_list.include?(recipe_name) || Array(self[:recipes]).include?(recipe_name)
248
248
  end
249
249
 
250
+ # used by include_recipe to add recipes to the expanded run_list to be
251
+ # saved back to the node and be searchable
252
+ def loaded_recipe(cookbook, recipe)
253
+ fully_qualified_recipe = "#{cookbook}::#{recipe}"
254
+ automatic_attrs[:recipes] << fully_qualified_recipe unless Array(self[:recipes]).include?(fully_qualified_recipe)
255
+ end
256
+
250
257
  # Returns true if this Node expects a given role, false if not.
251
258
  def role?(role_name)
252
259
  run_list.include?("role[#{role_name}]")
@@ -0,0 +1,49 @@
1
+ #
2
+ # Author:: Daniel DeLeo (<dan@getchef.com>)
3
+ # Copyright:: Copyright 2008-2014 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/policy_builder/expand_node_object'
20
+ require 'chef/policy_builder/policyfile'
21
+
22
+ class Chef
23
+
24
+ # PolicyBuilder contains classes that handles fetching policy from server or
25
+ # disk and resolving any indirection (e.g. expanding run_list).
26
+ #
27
+ # INPUTS
28
+ # * event stream object
29
+ # * node object/run_list
30
+ # * json_attribs
31
+ # * override_runlist
32
+ #
33
+ # OUTPUTS
34
+ # * mutated node object (implicit)
35
+ # * a new RunStatus (probably doesn't need to be here)
36
+ # * cookbooks sync'd to disk
37
+ # * cookbook_hash is stored in run_context
38
+ module PolicyBuilder
39
+
40
+ def self.strategy
41
+ if Chef::Config[:use_policyfile]
42
+ Policyfile
43
+ else
44
+ ExpandNodeObject
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,230 @@
1
+ #
2
+ # Author:: Adam Jacob (<adam@opscode.com>)
3
+ # Author:: Tim Hinderliter (<tim@opscode.com>)
4
+ # Author:: Christopher Walters (<cw@opscode.com>)
5
+ # Author:: Daniel DeLeo (<dan@getchef.com>)
6
+ # Copyright:: Copyright 2008-2014 Chef Software, Inc.
7
+ # License:: Apache License, Version 2.0
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+ #
21
+
22
+ require 'chef/log'
23
+ require 'chef/rest'
24
+ require 'chef/run_context'
25
+ require 'chef/config'
26
+ require 'chef/node'
27
+
28
+ class Chef
29
+ module PolicyBuilder
30
+
31
+ # ExpandNodeObject is the "classic" policy builder implementation. It
32
+ # expands the run_list on a node object and then queries the chef-server
33
+ # to find the correct set of cookbooks, given version constraints of the
34
+ # node's environment.
35
+ class ExpandNodeObject
36
+
37
+ attr_reader :events
38
+ attr_reader :node
39
+ attr_reader :node_name
40
+ attr_reader :ohai_data
41
+ attr_reader :json_attribs
42
+ attr_reader :override_runlist
43
+ attr_reader :original_runlist
44
+ attr_reader :run_context
45
+ attr_reader :run_list_expansion
46
+
47
+ def initialize(node_name, ohai_data, json_attribs, override_runlist, events)
48
+ @node_name = node_name
49
+ @ohai_data = ohai_data
50
+ @json_attribs = json_attribs
51
+ @override_runlist = override_runlist
52
+ @events = events
53
+
54
+ @node = nil
55
+ @original_runlist = nil
56
+ @run_list_expansion = nil
57
+ end
58
+
59
+ def setup_run_context(specific_recipes=nil)
60
+ if Chef::Config[:solo]
61
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, Chef::Config[:cookbook_path]) }
62
+ cl = Chef::CookbookLoader.new(Chef::Config[:cookbook_path])
63
+ cl.load_cookbooks
64
+ cookbook_collection = Chef::CookbookCollection.new(cl)
65
+ run_context = Chef::RunContext.new(node, cookbook_collection, @events)
66
+ else
67
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, api_service) }
68
+ cookbook_hash = sync_cookbooks
69
+ cookbook_collection = Chef::CookbookCollection.new(cookbook_hash)
70
+ run_context = Chef::RunContext.new(node, cookbook_collection, @events)
71
+ end
72
+
73
+ # TODO: this is not the place for this. It should be in Runner or
74
+ # CookbookCompiler or something.
75
+ run_context.load(@run_list_expansion)
76
+ if specific_recipes
77
+ specific_recipes.each do |recipe_file|
78
+ run_context.load_recipe_file(recipe_file)
79
+ end
80
+ end
81
+ run_context
82
+ end
83
+
84
+
85
+ # In client-server operation, loads the node state from the server. In
86
+ # chef-solo operation, builds a new node object.
87
+ def load_node
88
+ events.node_load_start(node_name, Chef::Config)
89
+ Chef::Log.debug("Building node object for #{node_name}")
90
+
91
+ if Chef::Config[:solo]
92
+ @node = Chef::Node.build(node_name)
93
+ else
94
+ @node = Chef::Node.find_or_create(node_name)
95
+ end
96
+ rescue Exception => e
97
+ # TODO: wrap this exception so useful error info can be given to the
98
+ # user.
99
+ events.node_load_failed(node_name, e, Chef::Config)
100
+ raise
101
+ end
102
+
103
+
104
+ # Applies environment, external JSON attributes, and override run list to
105
+ # the node, Then expands the run_list.
106
+ #
107
+ # === Returns
108
+ # node<Chef::Node>:: The modified node object. node is modified in place.
109
+ def build_node
110
+ # Allow user to override the environment of a node by specifying
111
+ # a config parameter.
112
+ if Chef::Config[:environment] && !Chef::Config[:environment].chop.empty?
113
+ node.chef_environment(Chef::Config[:environment])
114
+ end
115
+
116
+ # consume_external_attrs may add items to the run_list. Save the
117
+ # expanded run_list, which we will pass to the server later to
118
+ # determine which versions of cookbooks to use.
119
+ node.reset_defaults_and_overrides
120
+ node.consume_external_attrs(ohai_data, @json_attribs)
121
+
122
+ setup_run_list_override
123
+
124
+ expand_run_list
125
+
126
+ Chef::Log.info("Run List is [#{node.run_list}]")
127
+ Chef::Log.info("Run List expands to [#{@expanded_run_list_with_versions.join(', ')}]")
128
+
129
+ events.node_load_completed(node, @expanded_run_list_with_versions, Chef::Config)
130
+
131
+ node
132
+ end
133
+
134
+ # Expands the node's run list. Stores the run_list_expansion object for later use.
135
+ def expand_run_list
136
+ @run_list_expansion = if Chef::Config[:solo]
137
+ node.expand!('disk')
138
+ else
139
+ node.expand!('server')
140
+ end
141
+
142
+ # @run_list_expansion is a RunListExpansion.
143
+ #
144
+ # Convert @expanded_run_list, which is an
145
+ # Array of Hashes of the form
146
+ # {:name => NAME, :version_constraint => Chef::VersionConstraint },
147
+ # into @expanded_run_list_with_versions, an
148
+ # Array of Strings of the form
149
+ # "#{NAME}@#{VERSION}"
150
+ @expanded_run_list_with_versions = @run_list_expansion.recipes.with_version_constraints_strings
151
+ @run_list_expansion
152
+ rescue Exception => e
153
+ # TODO: wrap/munge exception with useful error output.
154
+ events.run_list_expand_failed(node, e)
155
+ raise
156
+ end
157
+
158
+ ########################################
159
+ # Internal public API
160
+ ########################################
161
+
162
+ # Sync_cookbooks eagerly loads all files except files and
163
+ # templates. It returns the cookbook_hash -- the return result
164
+ # from /environments/#{node.chef_environment}/cookbook_versions,
165
+ # which we will use for our run_context.
166
+ #
167
+ # === Returns
168
+ # Hash:: The hash of cookbooks with download URLs as given by the server
169
+ def sync_cookbooks
170
+ Chef::Log.debug("Synchronizing cookbooks")
171
+
172
+ begin
173
+ events.cookbook_resolution_start(@expanded_run_list_with_versions)
174
+ cookbook_hash = api_service.post("environments/#{node.chef_environment}/cookbook_versions",
175
+ {:run_list => @expanded_run_list_with_versions})
176
+ rescue Exception => e
177
+ # TODO: wrap/munge exception to provide helpful error output
178
+ events.cookbook_resolution_failed(@expanded_run_list_with_versions, e)
179
+ raise
180
+ else
181
+ events.cookbook_resolution_complete(cookbook_hash)
182
+ end
183
+
184
+ synchronizer = Chef::CookbookSynchronizer.new(cookbook_hash, events)
185
+ synchronizer.sync_cookbooks
186
+
187
+ # register the file cache path in the cookbook path so that CookbookLoader actually picks up the synced cookbooks
188
+ Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks")
189
+
190
+ cookbook_hash
191
+ end
192
+
193
+ def setup_run_list_override
194
+ runlist_override_sanity_check!
195
+ unless(override_runlist.empty?)
196
+ @original_runlist = node.run_list.run_list_items.dup
197
+ node.run_list(*override_runlist)
198
+ Chef::Log.warn "Run List override has been provided."
199
+ Chef::Log.warn "Original Run List: [#{original_runlist.join(', ')}]"
200
+ Chef::Log.warn "Overridden Run List: [#{node.run_list}]"
201
+ end
202
+ end
203
+
204
+ # Ensures runlist override contains RunListItem instances
205
+ def runlist_override_sanity_check!
206
+ # Convert to array and remove whitespace
207
+ if override_runlist.is_a?(String)
208
+ @override_runlist = override_runlist.split(',').map { |e| e.strip }
209
+ end
210
+ @override_runlist = [override_runlist].flatten.compact
211
+ override_runlist.map! do |item|
212
+ if(item.is_a?(Chef::RunList::RunListItem))
213
+ item
214
+ else
215
+ Chef::RunList::RunListItem.new(item)
216
+ end
217
+ end
218
+ end
219
+
220
+ def api_service
221
+ @api_service ||= Chef::REST.new(config[:chef_server_url])
222
+ end
223
+
224
+ def config
225
+ Chef::Config
226
+ end
227
+
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,338 @@
1
+ #
2
+ # Author:: Adam Jacob (<adam@opscode.com>)
3
+ # Author:: Tim Hinderliter (<tim@opscode.com>)
4
+ # Author:: Christopher Walters (<cw@opscode.com>)
5
+ # Author:: Daniel DeLeo (<dan@getchef.com>)
6
+ # Copyright:: Copyright 2008-2014 Chef Software, Inc.
7
+ # License:: Apache License, Version 2.0
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+ #
21
+
22
+ require 'chef/log'
23
+ require 'chef/rest'
24
+ require 'chef/run_context'
25
+ require 'chef/config'
26
+ require 'chef/node'
27
+
28
+ class Chef
29
+ module PolicyBuilder
30
+
31
+ # Policyfile is an experimental policy builder implementation that gets run
32
+ # list and cookbook version information from a single document.
33
+ #
34
+ # == WARNING
35
+ # This implementation is experimental. It may be changed in incompatible
36
+ # ways in minor or even patch releases, or even abandoned altogether. If
37
+ # using this with other tools, you may be forced to upgrade those tools in
38
+ # lockstep with chef-client because of incompatible behavior changes.
39
+ #
40
+ # == Unsupported Options:
41
+ # * override_runlist:: This could potentially be integrated into the
42
+ # policyfile, or replaced with a similar feature that has different
43
+ # semantics.
44
+ # * specific_recipes:: put more design thought into this use case.
45
+ # * run_list in json_attribs:: would be ignored anyway, so it raises an error.
46
+ # * chef-solo:: not currently supported. Need more design thought around
47
+ # how this should work.
48
+ class Policyfile
49
+
50
+ class UnsupportedFeature < StandardError; end
51
+
52
+ class PolicyfileError < StandardError; end
53
+
54
+ RunListExpansionIsh = Struct.new(:recipes, :roles)
55
+
56
+ attr_reader :events
57
+ attr_reader :node
58
+ attr_reader :node_name
59
+ attr_reader :ohai_data
60
+ attr_reader :json_attribs
61
+ attr_reader :run_context
62
+
63
+ def initialize(node_name, ohai_data, json_attribs, override_runlist, events)
64
+ @node_name = node_name
65
+ @ohai_data = ohai_data
66
+ @json_attribs = json_attribs
67
+ @events = events
68
+
69
+ @node = nil
70
+
71
+ Chef::Log.warn("Using experimental Policyfile feature")
72
+
73
+ if Chef::Config[:solo]
74
+ raise UnsupportedFeature, "Policyfile does not support chef-solo at this time."
75
+ end
76
+
77
+ if override_runlist
78
+ raise UnsupportedFeature, "Policyfile does not support override run lists at this time"
79
+ end
80
+
81
+ if json_attribs && json_attribs.key?("run_list")
82
+ raise UnsupportedFeature, "Policyfile does not support setting the run_list in json data at this time"
83
+ end
84
+
85
+ if Chef::Config[:environment] && !Chef::Config[:environment].chop.empty?
86
+ raise UnsupportedFeature, "Policyfile does not work with Chef Environments"
87
+ end
88
+ end
89
+
90
+ ## API Compat ##
91
+ # Methods related to unsupported features
92
+
93
+ # Override run_list is not supported.
94
+ def original_runlist
95
+ nil
96
+ end
97
+
98
+ # Override run_list is not supported.
99
+ def override_runlist
100
+ nil
101
+ end
102
+
103
+ # Policyfile gives you the run_list already expanded, but users of this
104
+ # class may expect to get a run_list expansion compatible object by
105
+ # calling this method.
106
+ #
107
+ # === Returns
108
+ # RunListExpansionIsh:: A RunListExpansion duck type
109
+ def run_list_expansion
110
+ run_list_expansion_ish
111
+ end
112
+
113
+ ## PolicyBuilder API ##
114
+
115
+ # Loads the node state from the server.
116
+ def load_node
117
+ events.node_load_start(node_name, Chef::Config)
118
+ Chef::Log.debug("Building node object for #{node_name}")
119
+
120
+ @node = Chef::Node.find_or_create(node_name)
121
+ validate_policyfile
122
+ node
123
+ rescue Exception => e
124
+ events.node_load_failed(node_name, e, Chef::Config)
125
+ raise
126
+ end
127
+
128
+ # Applies environment, external JSON attributes, and override run list to
129
+ # the node, Then expands the run_list.
130
+ #
131
+ # === Returns
132
+ # node<Chef::Node>:: The modified node object. node is modified in place.
133
+ def build_node
134
+ # consume_external_attrs may add items to the run_list. Save the
135
+ # expanded run_list, which we will pass to the server later to
136
+ # determine which versions of cookbooks to use.
137
+ node.reset_defaults_and_overrides
138
+
139
+ node.consume_external_attrs(ohai_data, json_attribs)
140
+
141
+ expand_run_list
142
+ apply_policyfile_attributes
143
+
144
+ Chef::Log.info("Run List is [#{run_list}]")
145
+ Chef::Log.info("Run List expands to [#{run_list_with_versions_for_display.join(', ')}]")
146
+
147
+
148
+ events.node_load_completed(node, run_list_with_versions_for_display, Chef::Config)
149
+
150
+ node
151
+ rescue Exception => e
152
+ events.node_load_failed(node_name, e, Chef::Config)
153
+ raise
154
+ end
155
+
156
+ def setup_run_context(specific_recipes=nil)
157
+ # TODO: This file vendor stuff is duplicated and initializing it with a
158
+ # block traps a reference to this object in a global context which will
159
+ # prevent it from getting GC'd. Simplify it.
160
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, api_service) }
161
+ sync_cookbooks
162
+ cookbook_collection = Chef::CookbookCollection.new(cookbooks_to_sync)
163
+ run_context = Chef::RunContext.new(node, cookbook_collection, events)
164
+
165
+ run_context.load(run_list_expansion_ish)
166
+
167
+ run_context
168
+ end
169
+
170
+ def expand_run_list
171
+ node.run_list(run_list)
172
+ node.automatic_attrs[:roles] = []
173
+ node.automatic_attrs[:recipes] = run_list_expansion_ish.recipes
174
+ run_list_expansion_ish
175
+ end
176
+
177
+ ## Internal Public API ##
178
+
179
+ def sync_cookbooks
180
+ Chef::Log.debug("Synchronizing cookbooks")
181
+ synchronizer = Chef::CookbookSynchronizer.new(cookbooks_to_sync, events)
182
+ synchronizer.sync_cookbooks
183
+
184
+ # register the file cache path in the cookbook path so that CookbookLoader actually picks up the synced cookbooks
185
+ Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks")
186
+
187
+ cookbooks_to_sync
188
+ end
189
+
190
+
191
+ def run_list_with_versions_for_display
192
+ run_list.map do |recipe_spec|
193
+ cookbook, recipe = parse_recipe_spec(recipe_spec)
194
+ lock_data = cookbook_lock_for(cookbook)
195
+ display = "#{cookbook}::#{recipe}@#{lock_data["version"]} (#{lock_data["identifier"][0...7]})"
196
+ display
197
+ end
198
+ end
199
+
200
+ def run_list_expansion_ish
201
+ recipes = run_list.map do |recipe_spec|
202
+ cookbook, recipe = parse_recipe_spec(recipe_spec)
203
+ "#{cookbook}::#{recipe}"
204
+ end
205
+ RunListExpansionIsh.new(recipes, [])
206
+ end
207
+
208
+ def apply_policyfile_attributes
209
+ node.attributes.role_default = policy["default_attributes"]
210
+ node.attributes.role_override = policy["override_attributes"]
211
+ end
212
+
213
+ def parse_recipe_spec(recipe_spec)
214
+ rmatch = recipe_spec.match(/recipe\[([^:]+)::([^:]+)\]/)
215
+ if rmatch.nil?
216
+ raise PolicyfileError, "invalid recipe specification #{recipe_spec} in Policyfile from #{policyfile_location}"
217
+ else
218
+ [rmatch[1], rmatch[2]]
219
+ end
220
+ end
221
+
222
+ def cookbook_lock_for(cookbook_name)
223
+ cookbook_locks[cookbook_name]
224
+ end
225
+
226
+ def run_list
227
+ policy["run_list"]
228
+ end
229
+
230
+ def policy
231
+ @policy ||= http_api.get(policyfile_location)
232
+ rescue Net::HTTPServerException => e
233
+ raise ConfigurationError, "Error loading policyfile from `#{policyfile_location}': #{e.class} - #{e.message}"
234
+ end
235
+
236
+ def policyfile_location
237
+ "data/policyfiles/#{deployment_group}"
238
+ end
239
+
240
+ # Do some mimimal validation of the policyfile we fetched from the
241
+ # server. Compatibility mode relies on using data bags to store policy
242
+ # files; therefore no real validation will be performed server-side and
243
+ # we need to make additional checks to ensure the data will be formatted
244
+ # correctly.
245
+ def validate_policyfile
246
+ errors = []
247
+ unless run_list
248
+ errors << "Policyfile is missing run_list element"
249
+ end
250
+ unless policy.key?("cookbook_locks")
251
+ errors << "Policyfile is missing cookbook_locks element"
252
+ end
253
+ if run_list.kind_of?(Array)
254
+ run_list_errors = run_list.select do |maybe_recipe_spec|
255
+ validate_recipe_spec(maybe_recipe_spec)
256
+ end
257
+ errors += run_list_errors
258
+ else
259
+ errors << "Policyfile run_list is malformed, must be an array of `recipe[cb_name::recipe_name]` items: #{policy["run_list"]}"
260
+ end
261
+
262
+ unless errors.empty?
263
+ raise PolicyfileError, "Policyfile fetched from #{policyfile_location} was invalid:\n#{errors.join("\n")}"
264
+ end
265
+ end
266
+
267
+ def validate_recipe_spec(recipe_spec)
268
+ parse_recipe_spec(recipe_spec)
269
+ nil
270
+ rescue PolicyfileError => e
271
+ e.message
272
+ end
273
+
274
+ class ConfigurationError < StandardError; end
275
+
276
+ def deployment_group
277
+ Chef::Config[:deployment_group] or
278
+ raise ConfigurationError, "Setting `deployment_group` is not configured."
279
+ end
280
+
281
+ # Builds a 'cookbook_hash' map of the form
282
+ # "COOKBOOK_NAME" => "IDENTIFIER"
283
+ #
284
+ # This can be passed to a Chef::CookbookSynchronizer object to
285
+ # synchronize the cookbooks.
286
+ #
287
+ # TODO: Currently this makes N API calls to the server to get the
288
+ # cookbook objects. With server support (bulk API or the like), this
289
+ # should be reduced to a single call.
290
+ def cookbooks_to_sync
291
+ @cookbook_to_sync ||= begin
292
+ events.cookbook_resolution_start(run_list_with_versions_for_display)
293
+
294
+ cookbook_versions_by_name = cookbook_locks.inject({}) do |cb_map, (name, lock_data)|
295
+ cb_map[name] = manifest_for(name, lock_data)
296
+ cb_map
297
+ end
298
+ events.cookbook_resolution_complete(cookbook_versions_by_name)
299
+
300
+ cookbook_versions_by_name
301
+ end
302
+ rescue Exception => e
303
+ # TODO: wrap/munge exception to provide helpful error output
304
+ events.cookbook_resolution_failed(run_list_with_versions_for_display, e)
305
+ raise
306
+ end
307
+
308
+ # Fetches the CookbookVersion object for the given name and identifer
309
+ # specified in the lock_data.
310
+ # TODO: This only implements Chef 11 compatibility mode, which means that
311
+ # cookbooks are fetched by the "dotted_decimal_identifier": a
312
+ # representation of a SHA1 in the traditional x.y.z version format.
313
+ def manifest_for(cookbook_name, lock_data)
314
+ xyz_version = lock_data["dotted_decimal_identifier"]
315
+ http_api.get("cookbooks/#{cookbook_name}/#{xyz_version}")
316
+ rescue Exception => e
317
+ message = "Error loading cookbook #{cookbook_name} at version #{xyz_version}: #{e.class} - #{e.message}"
318
+ err = Chef::Exceptions::CookbookNotFound.new(message)
319
+ err.set_backtrace(e.backtrace)
320
+ raise err
321
+ end
322
+
323
+ def cookbook_locks
324
+ policy["cookbook_locks"]
325
+ end
326
+
327
+ def http_api
328
+ @api_service ||= Chef::REST.new(config[:chef_server_url])
329
+ end
330
+
331
+ def config
332
+ Chef::Config
333
+ end
334
+
335
+ end
336
+ end
337
+ end
338
+