knife-topo 1.1.2 → 2.0.1

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.
@@ -0,0 +1,137 @@
1
+ #
2
+ # Author:: Christine Draper (<christine_draper@thirdwaveinsights.com>)
3
+ # Copyright:: Copyright (c) 2015 ThirdWave Insights LLC
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/data_bag_item'
20
+ require 'chef/topo/converter'
21
+ require 'chef/mixin/deep_merge'
22
+
23
+ class Chef
24
+ # Topology
25
+ class Topology < Chef::DataBagItem
26
+ attr_accessor :strategy
27
+
28
+ PRIORITIES = %w(default force_default normal override force_override)
29
+
30
+ # Have to override and say this is a data bag json_class
31
+ # or get error on upload re 'must specify id'
32
+ def to_json(*a)
33
+ result = {
34
+ 'name' => object_name,
35
+ 'json_class' => Chef::DataBagItem.name,
36
+ 'chef_type' => 'data_bag_item',
37
+ 'data_bag' => data_bag,
38
+ 'raw_data' => raw_data
39
+ }
40
+ Chef::JSONCompat.to_json(result, *a)
41
+ end
42
+
43
+ def self.convert_from(format, data)
44
+ from_json((Chef::Topo::Converter.convert(format, data)))
45
+ end
46
+
47
+ def self.from_json(data)
48
+ topo = new
49
+ topo.raw_data = data
50
+ topo
51
+ end
52
+
53
+ # Make sure the JSON has an id and other expected fields
54
+ def raw_data=(new_data)
55
+ new_data['id'] ||= (new_data['name'] || 'undefined')
56
+ new_data['nodes'] ||= []
57
+ super(normalize(new_data))
58
+ @strategy = raw_data['strategy'] || 'direct_to_node'
59
+ end
60
+
61
+ # clean up some variations so we only have to process one way
62
+ # in particular, allow 'attributes' as a synonym for 'normal'
63
+ def normalize(data)
64
+ data['nodes'] = data['nodes'].map do |n|
65
+ if n.key?('attributes')
66
+ n['normal'] = Chef::Mixin::DeepMerge.merge(
67
+ n['normal'], n['attributes']
68
+ )
69
+ n.delete('attributes')
70
+ end
71
+ n
72
+ end
73
+ data
74
+ end
75
+
76
+ def display_info
77
+ buildstamp = raw_data['buildstamp']
78
+ info = buildstamp ? ' buildstamp: ' + buildstamp : ''
79
+ display_name + info
80
+ end
81
+
82
+ def display_name
83
+ version = topo_version ? ' version: ' + topo_version : ''
84
+ topo_name + version
85
+ end
86
+
87
+ def topo_version
88
+ version = raw_data['version']
89
+ if version
90
+ version = version + '-' + raw_data['buildid'] if raw_data['buildid']
91
+ end
92
+ version
93
+ end
94
+
95
+ def topo_name
96
+ raw_data['name']
97
+ end
98
+
99
+ def nodes
100
+ raw_data['nodes']
101
+ end
102
+
103
+ # nodes with topo properties merged in
104
+ def merged_nodes
105
+ nodes.map do |n|
106
+ Chef::Mixin::DeepMerge.merge(node_defaults, n)
107
+ end
108
+ end
109
+
110
+ def node_defaults
111
+ defaults = {}
112
+ %w(chef_environment tags).each do |k|
113
+ defaults[k] = raw_data[k] if raw_data[k]
114
+ end
115
+
116
+ PRIORITIES.reverse_each do |p|
117
+ a = default_attrs(p)
118
+ defaults[p] = a if a
119
+ end
120
+ # Make sure we're not sharing objects
121
+ Mash.from_hash(Marshal.load(Marshal.dump(defaults)))
122
+ end
123
+
124
+ def default_attrs(priority)
125
+ return raw_data[priority] unless priority == 'normal'
126
+ add_topo_attrs(raw_data['normal'])
127
+ end
128
+
129
+ def add_topo_attrs(attrs)
130
+ a = attrs || {}
131
+ a['topo'] ||= {}
132
+ a['topo']['name'] = topo_name
133
+ a['topo']['node_type'] = a['node_type'] if a['node_type']
134
+ a
135
+ end
136
+ end
137
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knife-topo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christine Draper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-12-15 00:00:00.000000000 Z
11
+ date: 2016-01-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Knife-topo uses a JSON file to capture a topology of nodes, which can
14
14
  be loaded into Chef and bootstrapped
@@ -21,6 +21,14 @@ files:
21
21
  - LICENSE
22
22
  - README.md
23
23
  - knife-topo.gemspec
24
+ - lib/chef/knife/topo/bootstrap_helper.rb
25
+ - lib/chef/knife/topo/command_helper.rb
26
+ - lib/chef/knife/topo/consts.rb
27
+ - lib/chef/knife/topo/loader.rb
28
+ - lib/chef/knife/topo/node_update_helper.rb
29
+ - lib/chef/knife/topo/processor.rb
30
+ - lib/chef/knife/topo/processor/via_cookbook.rb
31
+ - lib/chef/knife/topo/processor/via_cookbook_print.rb
24
32
  - lib/chef/knife/topo/version.rb
25
33
  - lib/chef/knife/topo_bootstrap.rb
26
34
  - lib/chef/knife/topo_cookbook_create.rb
@@ -32,7 +40,9 @@ files:
32
40
  - lib/chef/knife/topo_list.rb
33
41
  - lib/chef/knife/topo_search.rb
34
42
  - lib/chef/knife/topo_update.rb
35
- - lib/chef/knife/topology_helper.rb
43
+ - lib/chef/topo/converter.rb
44
+ - lib/chef/topo/converter/topo_v1.rb
45
+ - lib/chef/topology.rb
36
46
  homepage: https://github.com/christinedraper/knife-topo
37
47
  licenses:
38
48
  - Apache License (2.0)
@@ -1,366 +0,0 @@
1
- #
2
- # Author:: Christine Draper (<christine_draper@thirdwaveinsights.com>)
3
- # Copyright:: Copyright (c) 2014 ThirdWave Insights LLC
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/data_bag'
20
- require 'chef/node'
21
- require 'chef/encrypted_data_bag_item'
22
- require 'chef/environment'
23
- require 'chef/knife/core/object_loader'
24
- require 'chef/rest'
25
-
26
- class Chef
27
- class Knife
28
- module TopologyHelper
29
-
30
- # load one or more topologies from file
31
- def load_topologies(topology_file_path)
32
-
33
- if ! topology_file_path.end_with?('.js', '.json')
34
- show_usage
35
- ui.fatal "TOPOLOGY_FILE must be a '.js' or '.json' file"
36
- exit(1)
37
- end
38
-
39
- topologies = loader.object_from_file(topology_file_path)
40
- topologies = [topologies] if !topologies.kind_of?(Array)
41
-
42
- topologies
43
- end
44
-
45
- # create the topology data bag
46
- def create_bag(bag_name)
47
- # check that the name is valid
48
- begin
49
- Chef::DataBag.validate_name!(bag_name)
50
- rescue Chef::Exceptions::InvalidDataBagName => e
51
- ui.fatal(e.message)
52
- exit(1)
53
- end
54
-
55
- # create the data bag
56
- begin
57
- data_bag = Chef::DataBag.new
58
- data_bag.name(bag_name)
59
- data_bag.create
60
- ui.info("Created topology data bag [#{bag_name}]")
61
- rescue Net::HTTPServerException => e
62
- raise unless e.to_s =~ /^409/
63
- data_bag = Chef::DataBag.load(bag_name)
64
- ui.info("Topology data bag #{bag_name} already exists")
65
- end
66
-
67
- data_bag
68
- end
69
-
70
- # make sure the chef environment exists
71
- def check_chef_env(chef_env_name)
72
-
73
- if chef_env_name
74
- begin
75
- chef_env = Chef::Environment.load(chef_env_name)
76
- rescue Net::HTTPServerException => e
77
- raise unless e.to_s =~ /^404/
78
- ui.info "Creating chef environment " + chef_env_name
79
- chef_env = Chef::Environment.new()
80
- chef_env.name(chef_env_name)
81
- chef_env.create
82
- end
83
- end
84
-
85
- chef_env
86
- end
87
-
88
- # recursive merge that retains all keys
89
- def prop_merge!(hash, other_hash)
90
- other_hash.each do |key, val|
91
- if val.kind_of?(Hash) && hash[key]
92
- prop_merge!(hash[key], val)
93
- else
94
- hash[key] = val
95
- end
96
- end
97
-
98
- hash
99
- end
100
-
101
- # Merges topology properties into nodes, returning the merged nodes
102
- def merge_topo_properties(nodes, topo_hash)
103
-
104
- if nodes && nodes.length > 0
105
- merged_nodes = nodes ? nodes.clone : []
106
- merged_nodes.each do |nodeprops|
107
-
108
- normal_defaults = topo_hash['normal'] ?
109
- Marshal.load(Marshal.dump(topo_hash['normal'])) : {}
110
- nodeprops['normal'] ||= {}
111
- nodeprops['normal'] = prop_merge!(normal_defaults, nodeprops['normal'])
112
- nodeprops['normal'] = prop_merge!(nodeprops['normal'], nodeprops['attributes']) if nodeprops['attributes']
113
- nodeprops['normal'] = prop_merge!(nodeprops['normal'], { "topo" => { "name" => topo_hash['name'] }})
114
-
115
- nodeprops['chef_environment'] ||= topo_hash['chef_environment'] if topo_hash['chef_environment']
116
-
117
- # merge in the topology tags
118
- nodeprops['tags'] ||= []
119
- nodeprops['tags'] |= topo_hash['tags'] if topo_hash['tags'] && topo_hash['tags'].length > 0
120
-
121
- end
122
- end
123
-
124
- merged_nodes
125
-
126
- end
127
-
128
-
129
- # Update an existing node
130
- def update_node(node_updates)
131
-
132
- config[:disable_editing] = true
133
-
134
- node_name = node_updates['name']
135
- begin
136
-
137
- # load then update and save the node
138
- node = Chef::Node.load(node_name)
139
-
140
- if node_updates['chef_environment'] && node_updates['chef_environment'] != node['chef_environment']
141
- check_chef_env(node_updates['chef_environment'])
142
- end
143
-
144
- if updated_values = update_node_with_values(node, node_updates)
145
- ui.info "Updating #{updated_values.join(', ')} on node #{node.name}"
146
- node.save
147
- ui.output(format_for_display(node)) if config[:print_after]
148
- else
149
- ui.info "No updates found for node #{node.name}"
150
- end
151
-
152
- rescue Net::HTTPServerException => e
153
- raise unless e.to_s =~ /^404/
154
- # Node has not been created
155
- end
156
-
157
- return node
158
- end
159
-
160
- # Make updates into the original node, returning the list of updated properties.
161
- def update_node_with_values(node, updates)
162
- updated_properties = []
163
-
164
- # merge the normal attributes (but not tags)
165
- normal_updates = updates['normal'] || {}
166
- normal_updates.delete('tags')
167
- original_normal = node.normal.clone()
168
- prop_merge!(node.normal, normal_updates)
169
- updated_properties << 'normal' if (original_normal != node.normal)
170
-
171
- # merge with existing runlist
172
- if updates['run_list']
173
- updated_run_list = RunList.new
174
- updates['run_list'].each { |e| updated_run_list << e }
175
- if (updated_run_list != node.run_list)
176
- updated_properties << 'run_list'
177
- node.run_list(*updated_run_list)
178
- end
179
- end
180
-
181
- # update chef env
182
- new_chef_environment = updates['chef_environment']
183
- if new_chef_environment && new_chef_environment != node.chef_environment
184
- updated_properties << 'chef_environment'
185
- node.chef_environment(new_chef_environment)
186
- end
187
-
188
- # merge tags
189
- orig_num_tags = node.tags.length
190
- updates['tags'] ||= [] # make sure tags are initialized
191
- node.tag(*updates['tags'])
192
- updated_properties << 'tags' if node.tags.length > orig_num_tags
193
-
194
- # return false if no updates, else return array of property names
195
- updated_properties.length > 0 && updated_properties
196
- end
197
-
198
- # Load a topology from local data bag item file
199
- def load_from_file(bag_name, topo_name)
200
-
201
- topo_file = File.join(Dir.pwd, "#{topologies_path}", bag_name, topo_name + '.json')
202
- return unless (loader.file_exists_and_is_readable?(topo_file))
203
-
204
- item_data = loader.object_from_file(topo_file)
205
- item_data = if use_encryption
206
- secret = read_secret
207
- Chef::EncryptedDataBagItem.encrypt_data_bag_item(item_data, secret)
208
- else
209
- item_data
210
- end
211
- item = Chef::DataBagItem.new
212
- item.data_bag(bag_name)
213
- item.raw_data = item_data
214
- item
215
- end
216
-
217
- # read in the topology bag item
218
- def load_from_server(bag_name, item_name = nil)
219
- begin
220
- if (item_name)
221
- item = Chef::DataBagItem.load(bag_name, item_name)
222
- item = Chef::EncryptedDataBagItem.new(item.raw_data, read_secret) if use_encryption
223
- else
224
- item = Chef::DataBag.load(bag_name)
225
- end
226
- rescue Net::HTTPServerException => e
227
- raise unless e.to_s =~ /^404/
228
- end
229
- item
230
- end
231
-
232
- # Replace existing run list in a node
233
- def set_run_list(node, entries)
234
- node.run_list.run_list_items.clear
235
- entries.each { |e| node.run_list << e }
236
- end
237
-
238
- # Name of the topology bag
239
- def topo_bag_name(name=nil)
240
- @topo_bag_name = name if (name)
241
- @topo_bag_name ||= "topologies"
242
- end
243
-
244
- # Path for the topologies data bags.
245
- # For now, use the standard data_bags path for our topologies bags
246
- def topologies_path
247
- @topologies_path ||= "data_bags"
248
- end
249
-
250
- # Loader to get data bag items from file
251
- def loader
252
- @loader ||= Knife::Core::ObjectLoader.new(DataBagItem, ui)
253
- end
254
-
255
- # Determine if the bag items are/should be encrypted on server
256
- # NOTE: This option isnt currently enabled
257
- def use_encryption
258
- if config[:secret] && config[:secret_file]
259
- ui.fatal("please specify only one of --secret, --secret-file")
260
- exit(1)
261
- end
262
- config[:secret] || config[:secret_file]
263
- end
264
-
265
- # Return the secret key to encrypt/decrypt data bag items
266
- def read_secret
267
- if config[:secret]
268
- config[:secret]
269
- else
270
- Chef::EncryptedDataBagItem.load_secret(config[:secret_file])
271
- end
272
- end
273
-
274
- # initialize args for another knife command
275
- def initialize_cmd_args(args, new_name_args)
276
- args = args.dup
277
- args.shift(2 + @name_args.length)
278
- cmd_args = new_name_args + args
279
- end
280
-
281
- # run another knife command
282
- def run_cmd(command_class, args)
283
- command = command_class.new(args)
284
- command.config[:config_file] = config[:config_file]
285
- command.configure_chef
286
- command.run
287
-
288
- command
289
- end
290
-
291
- # upload cookbooks - will warn and continue if upload fails (e.g. may be frozen)
292
- def upload_cookbooks(args)
293
- begin
294
- run_cmd(Chef::Knife::TopoCookbookUpload, args)
295
- rescue Exception => e
296
- raise if Chef::Config[:verbosity] == 2
297
- end
298
- end
299
-
300
- def display_name (topo)
301
- topo['name'] + ((topo['version']) ? " version " + format_topo_version(topo) : "")
302
- end
303
-
304
- # Topology version
305
- def format_topo_version(topo)
306
- version = nil
307
- if topo['version']
308
- version = topo['version']
309
- version = version + '-' + topo['buildid'] if (topo['buildid'])
310
- end
311
-
312
- version
313
- end
314
-
315
- # check if resource exists
316
- def resource_exists?(relative_path)
317
- rest.get_rest(relative_path)
318
- true
319
- rescue Net::HTTPServerException => e
320
- raise unless e.response.code == "404"
321
- false
322
- end
323
-
324
- # Setup the bootstrap args and run the bootstrap command
325
- def run_bootstrap(node_data, bootstrap_args, overwrite=false)
326
- node_name = node_data['name']
327
-
328
- args = bootstrap_args
329
-
330
- # We need to remove the --bootstrap option, if it exists, because its not valid for knife bootstrap
331
- args -= ['--bootstrap']
332
-
333
- # And set up the node-specific data
334
- args += ['-N', node_name] if(node_name)
335
- args += ['-E', node_data['chef_environment']] if(node_data['chef_environment'])
336
- args[1] = node_data['ssh_host']
337
- args += [ '--ssh-port', node_data['ssh_port']] if node_data['ssh_port']
338
- args += [ '--run-list' , node_data['run_list'].join(',')] if node_data['run_list']
339
- args += [ '--json-attributes' , node_data['normal'].to_json] if node_data['normal']
340
-
341
- if overwrite
342
- ui.info("Node #{node_name} exists and will be overwritten")
343
- # delete node first so vault refresh does not pick up existing node
344
- begin
345
- rest.delete("nodes/#{node_name}")
346
- rest.delete("clients/#{node_name}")
347
- rescue Net::HTTPServerException => e
348
- raise unless e.response.code == "404"
349
- end
350
- end
351
-
352
- ui.info "Bootstrapping node #{node_name}"
353
- begin
354
- run_cmd(Chef::Knife::Bootstrap, args)
355
- true
356
- rescue Exception => e
357
- raise if Chef::Config[:verbosity] == 2
358
- ui.warn "bootstrap of node #{node_name} exited with error"
359
- humanize_exception(e)
360
- false
361
- end
362
- end
363
-
364
- end
365
- end
366
- end