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.
@@ -4,17 +4,17 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'chef/knife/topo/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "knife-topo"
8
- spec.version = Knife::Topo::VERSION
9
- spec.authors = ["Christine Draper"]
10
- spec.email = ["christine_draper@thirdwaveinsights.com"]
11
- spec.summary = "Knife plugin to manage topologies of nodes"
12
- spec.description = "Knife-topo uses a JSON file to capture a topology of nodes, which can be loaded into Chef and bootstrapped"
13
- spec.homepage = "https://github.com/christinedraper/knife-topo"
14
- spec.license = "Apache License (2.0)"
15
-
16
- spec.files = Dir.glob("{lib}/**/*") +
17
- ['LICENSE', 'README.md', __FILE__]
18
- spec.require_paths = ["lib"]
7
+ spec.name = 'knife-topo'
8
+ spec.version = KnifeTopo::VERSION
9
+ spec.authors = ['Christine Draper']
10
+ spec.email = ['christine_draper@thirdwaveinsights.com']
11
+ spec.summary = 'Knife plugin to manage topologies of nodes'
12
+ spec.description = 'Knife-topo uses a JSON file to capture a topology '\
13
+ 'of nodes, which can be loaded into Chef and bootstrapped'
14
+ spec.homepage = 'https://github.com/christinedraper/knife-topo'
15
+ spec.license = 'Apache License (2.0)'
19
16
 
17
+ spec.files = Dir.glob('{lib}/**/*') +
18
+ ['LICENSE', 'README.md', __FILE__]
19
+ spec.require_paths = ['lib']
20
20
  end
@@ -0,0 +1,75 @@
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/rest'
20
+ require 'chef/knife/topo/command_helper'
21
+
22
+ module KnifeTopo
23
+ # Node update helper for knife topo
24
+ module BootstrapHelper
25
+ include KnifeTopo::CommandHelper
26
+
27
+ # Setup the bootstrap args and run the bootstrap command
28
+ def run_bootstrap(data, bootstrap_args, overwrite = false)
29
+ node_name = data['name']
30
+ args = setup_bootstrap_args(bootstrap_args, data)
31
+ delete_client_node(node_name) if overwrite
32
+
33
+ ui.info "Bootstrapping node #{node_name}"
34
+ run_cmd(Chef::Knife::Bootstrap, args)
35
+ rescue StandardError => e
36
+ raise if Chef::Config[:verbosity] == 2
37
+ ui.warn "bootstrap of node #{node_name} exited with error"
38
+ humanize_exception(e)
39
+ false
40
+ end
41
+
42
+ # rubocop:disable Metrics/AbcSize
43
+ def setup_bootstrap_args(args, data)
44
+ # We need to remove the --bootstrap option, if it exists
45
+ args -= ['--bootstrap']
46
+ args[1] = data['ssh_host']
47
+
48
+ # And set up the node-specific data but ONLY if defined
49
+ args += ['-N', data['name']] if data['name']
50
+ args += ['-E', data['chef_environment']] if data['chef_environment']
51
+ args += ['--ssh-port', data['ssh_port']] if data['ssh_port']
52
+ args += ['--run-list', data['run_list'].join(',')] if data['run_list']
53
+ attrs = attributes_for_bootstrap(data)
54
+ args += ['--json-attributes', attrs.to_json] unless attrs.empty?
55
+ args
56
+ end
57
+ # rubocop:enable Metrics/AbcSize
58
+
59
+ # for bootstrap, attributes have to include tags
60
+ def attributes_for_bootstrap(data)
61
+ attrs = data['normal'] || {}
62
+ attrs['tags'] = data['tags'] if data['tags']
63
+ attrs
64
+ end
65
+
66
+ def delete_client_node(node_name)
67
+ ui.info("Node #{node_name} exists and will be overwritten")
68
+ # delete node first so vault refresh does not pick up existing node
69
+ rest.delete("nodes/#{node_name}")
70
+ rest.delete("clients/#{node_name}")
71
+ rescue Net::HTTPServerException => e
72
+ raise unless e.response.code == '404'
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,71 @@
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/environment'
20
+
21
+ module KnifeTopo
22
+ # Knife topo helpers
23
+ module CommandHelper
24
+ # initialize args for another knife command
25
+ def initialize_cmd_args(args, name_args, new_name_args)
26
+ args = args.dup
27
+ args.shift(2 + name_args.length)
28
+ new_name_args + args
29
+ end
30
+
31
+ # run another knife command
32
+ def run_cmd(command_class, args)
33
+ command = command_class.new(args)
34
+ command.config[:config_file] = config[:config_file]
35
+ command.configure_chef
36
+ command_class.load_deps
37
+ command.run
38
+
39
+ command
40
+ end
41
+
42
+ # check if resource exists
43
+ def resource_exists?(relative_path)
44
+ rest.get_rest(relative_path)
45
+ true
46
+ rescue Net::HTTPServerException => e
47
+ raise unless e.response.code == '404'
48
+ false
49
+ end
50
+
51
+ # make sure the chef environment exists
52
+ def check_chef_env(chef_env_name)
53
+ return unless chef_env_name
54
+ Chef::Environment.load(chef_env_name) if chef_env_name
55
+ rescue Net::HTTPServerException => e
56
+ raise unless e.to_s =~ /^404/
57
+ ui.info 'Creating chef environment ' + chef_env_name
58
+ chef_env = Chef::Environment.new
59
+ chef_env.name(chef_env_name)
60
+ chef_env.create
61
+ chef_env
62
+ end
63
+
64
+ def most_common(vals)
65
+ return if vals.length == 0
66
+ vals.group_by do |val|
67
+ val
68
+ end.values.max_by(&:size).first
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,4 @@
1
+ # constants
2
+ module KnifeTopo
3
+ PRIORITIES = %w(default force_default normal override force_override)
4
+ end
@@ -0,0 +1,137 @@
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/topology'
20
+ require 'chef/knife/core/object_loader'
21
+ require 'chef/data_bag'
22
+ require 'chef/data_bag_item'
23
+ require 'chef/knife'
24
+
25
+ module KnifeTopo
26
+ # Topology loaders
27
+ module Loader
28
+ attr_reader :ui, :loader
29
+
30
+ # Loader to get data bag items from file
31
+ def loader
32
+ @loader ||= Chef::Knife::Core::ObjectLoader.new(Chef::DataBagItem, ui)
33
+ end
34
+
35
+ def load_local_topo_or_exit(topo_name)
36
+ filepath = get_local_topo_path(topo_name)
37
+ msg = "Topology file #{filepath} not found - use " \
38
+ "'knife topo import' first"
39
+ check_file(filepath, msg)
40
+ load_topo_from_file_or_exit(filepath)
41
+ end
42
+
43
+ def load_topo_from_file_or_exit(filepath, format = nil)
44
+ check_file(filepath)
45
+ data = loader.object_from_file(filepath)
46
+ format ||= auto_detect_format(data)
47
+ topo = Chef::Topology.convert_from(format, data)
48
+ topo.data_bag(topo_bag_name)
49
+ topo
50
+ end
51
+
52
+ def check_file(filepath, msg = nil)
53
+ return if loader.file_exists_and_is_readable?(filepath)
54
+ msg ||= "Topology file #{filepath} not found"
55
+ ui.fatal(msg)
56
+ exit(1)
57
+ end
58
+
59
+ def auto_detect_format(data)
60
+ return 'topo_v1' if data['cookbook_attributes']
61
+ 'default'
62
+ end
63
+
64
+ def get_local_topo_path(topo_name)
65
+ File.join(
66
+ Dir.pwd,
67
+ topologies_path,
68
+ topo_bag_name,
69
+ topo_name + '.json'
70
+ )
71
+ end
72
+
73
+ def load_topo_from_server(topo_name)
74
+ Chef::Topology.load(topo_bag_name, topo_name)
75
+ rescue Net::HTTPServerException => e
76
+ raise unless e.to_s =~ /^404/
77
+ end
78
+
79
+ def load_topo_from_server_or_exit(topo_name)
80
+ topo = load_topo_from_server(topo_name)
81
+ unless topo
82
+ ui.fatal("Topology #{topo_bag_name}/#{@topo_name} does not exist " \
83
+ "on the server - use 'knife topo create' first")
84
+ exit(1)
85
+ end
86
+ topo
87
+ end
88
+
89
+ # Name of the topology bag
90
+ def topo_bag_name
91
+ @topo_bag_name ||= config[:data_bag]
92
+ @topo_bag_name ||= 'topologies'
93
+ end
94
+
95
+ # Path for the topologies data bags.
96
+ # For now, use the standard data_bags path for our topologies bags
97
+ def topologies_path
98
+ @topologies_path ||= 'data_bags'
99
+ end
100
+
101
+ def create_topo_bag
102
+ data_bag = Chef::DataBag.new
103
+ data_bag.name(topo_bag_name)
104
+ data_bag.create
105
+ rescue Net::HTTPServerException => e
106
+ raise unless e.to_s =~ /^409/
107
+ end
108
+
109
+ def list_topo_bag
110
+ Chef::DataBag.load(topo_bag_name)
111
+ rescue Net::HTTPServerException => e
112
+ raise unless e.to_s =~ /^404/
113
+ end
114
+
115
+ def load_node_data(node_name, min_priority = 'default')
116
+ node_data = {}
117
+ node = Chef::Node.load(node_name)
118
+ %w(name tags chef_environment run_list).each do |key|
119
+ node_data[key] = node.send(key)
120
+ end
121
+ node_data = node_data.merge(priority_attrs(node, min_priority))
122
+ end
123
+
124
+ def priority_attrs(node, min_priority = 'default')
125
+ attrs = {}
126
+ p = KnifeTopo::PRIORITIES
127
+ min_index = p.index(min_priority)
128
+ p.each_index do |index|
129
+ next if index < min_index
130
+ key = p[index]
131
+ attrs[key] = node.send(key)
132
+ attrs.delete(key) if attrs[key].empty?
133
+ end
134
+ attrs
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,111 @@
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/node'
20
+ require 'chef/run_list'
21
+ require 'chef/mixin/deep_merge'
22
+ require 'chef/rest'
23
+
24
+ module KnifeTopo
25
+ # Node update helper for knife topo
26
+ module NodeUpdateHelper
27
+ # Update an existing node
28
+ def update_node(node_updates)
29
+ config[:disable_editing] = true
30
+
31
+ begin
32
+ # load then update and save the node
33
+ node = Chef::Node.load(node_updates['name'])
34
+
35
+ env = node_updates['chef_environment']
36
+ check_chef_env(env) unless env == node['chef_environment']
37
+ do_node_updates(node, node_updates)
38
+
39
+ rescue Net::HTTPServerException => e
40
+ raise unless e.to_s =~ /^404/
41
+ # Node has not been created
42
+ end
43
+
44
+ node
45
+ end
46
+
47
+ def do_node_updates(node, node_updates)
48
+ updated = update_node_with_values(node, node_updates)
49
+ if updated
50
+ ui.info "Updating #{updated.join(', ')} on node #{node.name}"
51
+ node.save
52
+ ui.output(format_for_display(node)) if config[:print_after]
53
+ else
54
+ ui.info "No updates found for node #{node.name}"
55
+ end
56
+ end
57
+
58
+ # Update original node, return list of updated properties.
59
+ def update_node_with_values(node, updates)
60
+ updated = []
61
+
62
+ # merge the normal attributes (but not tags)
63
+ updated << 'normal' if update_attrs(node, updates['normal'])
64
+
65
+ # update runlist
66
+ updated << 'run_list' if update_runlist(node, updates['run_list'])
67
+
68
+ # update chef env
69
+ if update_chef_env(node, updates['chef_environment'])
70
+ updated << 'chef_environment'
71
+ end
72
+
73
+ # merge tags
74
+ updated << 'tags' if update_tags(node, updates['tags'])
75
+
76
+ # return false if no updates, else return array of property names
77
+ updated.length > 0 && updated
78
+ end
79
+
80
+ # Update methods all return true if an actual update is made
81
+ def update_attrs(node, attrs)
82
+ return false unless attrs
83
+ attrs.delete('tags')
84
+ original = node.normal.clone
85
+ node.normal = Chef::Mixin::DeepMerge.merge(node.normal, attrs)
86
+ original != node.normal
87
+ end
88
+
89
+ def update_runlist(node, runlist)
90
+ # node.run_list MUST be lhs of != to use override operator
91
+ return false unless runlist && node.run_list != runlist
92
+ updated_run_list = Chef::RunList.new
93
+ runlist.each { |e| updated_run_list << e }
94
+ node.run_list(*updated_run_list)
95
+ true
96
+ end
97
+
98
+ def update_chef_env(node, env)
99
+ return false unless env && env != node.chef_environment
100
+ node.chef_environment(env)
101
+ true
102
+ end
103
+
104
+ def update_tags(node, tags)
105
+ return false unless tags
106
+ orig_num_tags = node.tags.length
107
+ node.tag(*tags)
108
+ node.tags.length > orig_num_tags
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,76 @@
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
+ #
20
+ # The processor class converts a topology into node data and artifacts,
21
+ # based on the strategy
22
+ #
23
+
24
+ require 'chef/mixin/deep_merge'
25
+ module KnifeTopo
26
+ # Base processor
27
+ class Processor
28
+ # rubocop:disable Style/ClassVars
29
+ @@processor_classes = {}
30
+
31
+ def self.register_processor(strategy, class_name)
32
+ @@processor_classes[strategy] = class_name
33
+ end
34
+
35
+ # Get the right processor
36
+ def self.processor(topo)
37
+ strategy = topo.strategy
38
+ processor_class = @@processor_classes[strategy]
39
+ processor_class = load_processor(strategy) unless processor_class
40
+
41
+ Object.const_get(processor_class).new(topo)
42
+ end
43
+
44
+ def self.load_processor(strategy)
45
+ require "chef/knife/topo/processor/#{strategy}"
46
+ @@processor_classes[strategy]
47
+ rescue LoadError
48
+ STDERR.puts("#{strategy} is not a known strategy")
49
+ exit(1)
50
+ end
51
+
52
+ def self.for_topo(topo)
53
+ processor(topo)
54
+ end
55
+
56
+ attr_accessor :input
57
+
58
+ def initialize(topo)
59
+ @topo = topo
60
+ end
61
+
62
+ # Other processors should override the following methods
63
+ register_processor('direct_to_node', name)
64
+
65
+ def generate_nodes
66
+ @topo.merged_nodes
67
+ end
68
+
69
+ def generate_artifacts(_context = {})
70
+ {}
71
+ end
72
+
73
+ def upload_artifacts(_context = {})
74
+ end
75
+ end
76
+ end