1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
|
#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Daniel DeLeo (<dan@chef.io>)
# Copyright:: Copyright 2008-2016 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require "chef/log"
require "chef/server_api"
require "chef/run_context"
require "chef/config"
require "chef/node"
require "chef/chef_class"
class Chef
module PolicyBuilder
# ExpandNodeObject is the "classic" policy builder implementation. It
# expands the run_list on a node object and then queries the chef-server
# to find the correct set of cookbooks, given version constraints of the
# node's environment.
#
# Note that this class should only be used via PolicyBuilder::Dynamic and
# not instantiated directly.
class ExpandNodeObject
attr_reader :events
attr_reader :node
attr_reader :node_name
attr_reader :ohai_data
attr_reader :json_attribs
attr_reader :override_runlist
attr_reader :run_context
attr_reader :run_list_expansion
def initialize(node_name, ohai_data, json_attribs, override_runlist, events)
@node_name = node_name
@ohai_data = ohai_data
@json_attribs = json_attribs
@override_runlist = override_runlist
@events = events
@node = nil
@run_list_expansion = nil
end
# This method injects the run_context and into the Chef class.
#
# NOTE: This is duplicated with the Policyfile implementation. If
# it gets any more complicated, it needs to be moved elsewhere.
#
# @param run_context [Chef::RunContext] the run_context to inject
def setup_chef_class(run_context)
Chef.set_run_context(run_context)
end
def setup_run_context(specific_recipes = nil)
if Chef::Config[:solo]
Chef::Cookbook::FileVendor.fetch_from_disk(Chef::Config[:cookbook_path])
cl = Chef::CookbookLoader.new(Chef::Config[:cookbook_path])
cl.load_cookbooks
cookbook_collection = Chef::CookbookCollection.new(cl)
cookbook_collection.validate!
cookbook_collection.install_gems(events)
run_context = Chef::RunContext.new(node, cookbook_collection, @events)
else
Chef::Cookbook::FileVendor.fetch_from_remote(api_service)
cookbook_hash = sync_cookbooks
cookbook_collection = Chef::CookbookCollection.new(cookbook_hash)
cookbook_collection.validate!
cookbook_collection.install_gems(events)
run_context = Chef::RunContext.new(node, cookbook_collection, @events)
end
# TODO: this is really obviously not the place for this
# FIXME: need same edits
setup_chef_class(run_context)
# TODO: this is not the place for this. It should be in Runner or
# CookbookCompiler or something.
run_context.load(@run_list_expansion)
if specific_recipes
specific_recipes.each do |recipe_file|
run_context.load_recipe_file(recipe_file)
end
end
run_context
end
# DEPRECATED: As of Chef 12.5, chef selects either policyfile mode or
# "expand node" mode dynamically, based on the content of the node
# object, first boot JSON, and config. This happens in
# PolicyBuilder::Dynamic, which selects the implementation during
# #load_node and then delegates to either ExpandNodeObject or Policyfile
# implementations as appropriate. Tools authors should update their code
# to create a PolicyBuilder::Dynamc policy builder and allow it to select
# the proper implementation.
def load_node
Chef.log_deprecation("ExpandNodeObject#load_node is deprecated. Please use Chef::PolicyBuilder::Dynamic instead of using ExpandNodeObject directly")
events.node_load_start(node_name, config)
Chef::Log.debug("Building node object for #{node_name}")
@node =
if Chef::Config[:solo]
Chef::Node.build(node_name)
else
Chef::Node.find_or_create(node_name)
end
finish_load_node(node)
node
rescue Exception => e
events.node_load_failed(node_name, e, config)
raise
end
def finish_load_node(node)
@node = node
end
# Applies environment, external JSON attributes, and override run list to
# the node, Then expands the run_list.
#
# === Returns
# node<Chef::Node>:: The modified node object. node is modified in place.
def build_node
# Allow user to override the environment of a node by specifying
# a config parameter.
if Chef::Config[:environment] && !Chef::Config[:environment].chomp.empty?
node.chef_environment(Chef::Config[:environment])
end
# consume_external_attrs may add items to the run_list. Save the
# expanded run_list, which we will pass to the server later to
# determine which versions of cookbooks to use.
node.reset_defaults_and_overrides
node.consume_external_attrs(ohai_data, @json_attribs)
setup_run_list_override
expand_run_list
Chef::Log.info("Run List is [#{node.run_list}]")
Chef::Log.info("Run List expands to [#{@expanded_run_list_with_versions.join(', ')}]")
events.node_load_completed(node, @expanded_run_list_with_versions, Chef::Config)
events.run_list_expanded(@run_list_expansion)
node
end
# Expands the node's run list. Stores the run_list_expansion object for later use.
def expand_run_list
@run_list_expansion = if Chef::Config[:solo]
node.expand!("disk")
else
node.expand!("server")
end
# @run_list_expansion is a RunListExpansion.
#
# Convert @expanded_run_list, which is an
# Array of Hashes of the form
# {:name => NAME, :version_constraint => Chef::VersionConstraint },
# into @expanded_run_list_with_versions, an
# Array of Strings of the form
# "#{NAME}@#{VERSION}"
@expanded_run_list_with_versions = @run_list_expansion.recipes.with_version_constraints_strings
@run_list_expansion
rescue Exception => e
# TODO: wrap/munge exception with useful error output.
events.run_list_expand_failed(node, e)
raise
end
# Sync_cookbooks eagerly loads all files except files and
# templates. It returns the cookbook_hash -- the return result
# from /environments/#{node.chef_environment}/cookbook_versions,
# which we will use for our run_context.
#
# === Returns
# Hash:: The hash of cookbooks with download URLs as given by the server
def sync_cookbooks
Chef::Log.debug("Synchronizing cookbooks")
begin
events.cookbook_resolution_start(@expanded_run_list_with_versions)
cookbook_hash = api_service.post("environments/#{node.chef_environment}/cookbook_versions",
{ :run_list => @expanded_run_list_with_versions })
cookbook_hash = cookbook_hash.inject({}) do |memo, (key, value)|
memo[key] = Chef::CookbookVersion.from_hash(value)
memo
end
rescue Exception => e
# TODO: wrap/munge exception to provide helpful error output
events.cookbook_resolution_failed(@expanded_run_list_with_versions, e)
raise
else
events.cookbook_resolution_complete(cookbook_hash)
end
synchronizer = Chef::CookbookSynchronizer.new(cookbook_hash, events)
if temporary_policy?
synchronizer.remove_obsoleted_files = false
end
synchronizer.sync_cookbooks
# register the file cache path in the cookbook path so that CookbookLoader actually picks up the synced cookbooks
Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks")
cookbook_hash
end
# Indicates whether the policy is temporary, which means an
# override_runlist was provided. Chef::Client uses this to decide whether
# to do the final node save at the end of the run or not.
def temporary_policy?
!node.override_runlist.empty?
end
########################################
# Internal public API
########################################
def setup_run_list_override
runlist_override_sanity_check!
unless override_runlist.empty?
node.override_runlist(*override_runlist)
Chef::Log.warn "Run List override has been provided."
Chef::Log.warn "Original Run List: [#{node.primary_runlist}]"
Chef::Log.warn "Overridden Run List: [#{node.run_list}]"
end
end
# Ensures runlist override contains RunListItem instances
def runlist_override_sanity_check!
# Convert to array and remove whitespace
if override_runlist.is_a?(String)
@override_runlist = override_runlist.split(",").map { |e| e.strip }
end
@override_runlist = [override_runlist].flatten.compact
override_runlist.map! do |item|
if item.is_a?(Chef::RunList::RunListItem)
item
else
Chef::RunList::RunListItem.new(item)
end
end
end
def api_service
@api_service ||= Chef::ServerAPI.new(config[:chef_server_url])
end
def config
Chef::Config
end
end
end
end
|