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,118 @@
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
+ # Converts data in a given format into V2 topo JSON format
21
+ #
22
+ require 'chef/knife/topo/processor'
23
+ require 'chef/knife/topo/processor/via_cookbook_print'
24
+ require 'chef/knife/topo/command_helper'
25
+ require 'chef/knife/cookbook_upload' unless defined? Chef::Knife::CookbookUpload
26
+ require 'chef/knife/cookbook_create'
27
+
28
+ module KnifeTopo
29
+ # Class to provide context to execute knife topo helper methods
30
+ class KnifeHelper < Chef::Knife
31
+ include KnifeTopo::CommandHelper
32
+ end
33
+
34
+ # Process attributes via a cookbook
35
+ class ViaCookbookProcessor < KnifeTopo::Processor
36
+ attr_accessor :cookbook, :filename
37
+
38
+ include KnifeTopo::ViaCookbookPrint
39
+
40
+ def initialize(topo)
41
+ super
42
+ data = @topo['strategy_data'] || {}
43
+ @cookbook = data['cookbook'] || topo.topo_name
44
+ @filename = data['filename'] || 'topology'
45
+ @helper = KnifeHelper.new
46
+ end
47
+
48
+ register_processor('via_cookbook', name)
49
+
50
+ def generate_nodes
51
+ super
52
+ end
53
+
54
+ # generate attributes to cookbook
55
+ # context['cmd'] must be calling command
56
+ # context['cmd_args'] must be calling command's args
57
+ def generate_artifacts(context = {})
58
+ @cmd = context['cmd']
59
+ @cmd_args = context['cmd_args'] || []
60
+ @config = Chef::Config.merge!(@cmd.config)
61
+ return unless @cmd && cookbook_path
62
+ run_create_cookbook
63
+ create_attr_file(
64
+ cookbook_path,
65
+ cookbook_contents
66
+ )
67
+ end
68
+
69
+ def run_create_cookbook
70
+ create_args = @helper.initialize_cmd_args(
71
+ @cmd_args, @cmd.name_args, %w(cookbook create)
72
+ )
73
+ create_args[2] = @cookbook
74
+ # set options from calling command, so validation does not fail
75
+ Chef::Knife::CookbookCreate.options = @cmd.class.options
76
+ @helper.run_cmd(Chef::Knife::CookbookCreate, create_args)
77
+ rescue StandardError => e
78
+ raise if Chef::Config[:verbosity] == 2
79
+ @helper.ui.warn "Create of cookbook #{@cookbook} exited with error"
80
+ @helper.humanize_exception(e)
81
+ end
82
+
83
+ def create_attr_file(dir, contents)
84
+ @helper.ui.info("** Creating attribute file: #{@filename}")
85
+
86
+ name = @filename << '.rb' unless File.extname(@filename) == '.rb'
87
+ filepath = File.join(dir, @cookbook, 'attributes', name)
88
+ File.open(filepath, 'w') { |file| file.write(contents) }
89
+ end
90
+
91
+ def cookbook_path
92
+ paths = @config['cookbook_path']
93
+ return unless paths
94
+ paths.first
95
+ end
96
+
97
+ def upload_artifacts(context = {})
98
+ @cmd = context['cmd']
99
+ @cmd_args = context['cmd_args'] || []
100
+ return unless @cmd && !@cmd.config[:disable_upload]
101
+ run_upload_cookbook
102
+ end
103
+
104
+ def run_upload_cookbook
105
+ upload_args = @helper.initialize_cmd_args(
106
+ @cmd_args, @cmd.name_args, %w(cookbook upload)
107
+ )
108
+ upload_args[2] = @cookbook
109
+ # set options from calling command, so validation does not fail
110
+ Chef::Knife::CookbookUpload.options = @cmd.class.options
111
+ @helper.run_cmd(Chef::Knife::CookbookUpload, upload_args)
112
+ rescue StandardError => e
113
+ raise if Chef::Config[:verbosity] == 2
114
+ @helper.ui.warn "Upload of cookbook #{@cookbook} exited with error"
115
+ @helper.humanize_exception(e)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,97 @@
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
+ require 'chef/knife/topo/consts'
19
+
20
+ module KnifeTopo
21
+ # Prints attribute file contents to string
22
+ module ViaCookbookPrint
23
+ def print_attr_header(contents, cookbook_name, filename)
24
+ copyright = @config['cookbook_copyright']
25
+ contents << "
26
+ #
27
+ # THIS FILE IS GENERATED BY KNIFE TOPO - MANUAL CHANGES WILL BE OVERWRITTEN
28
+ #
29
+ # Cookbook Name:: #{cookbook_name}
30
+ # Attribute File:: #{filename}
31
+ #
32
+ "
33
+ return unless copyright
34
+ contents << "
35
+ # Copyright (c) #{Time.now.year} #{copyright}
36
+ #
37
+ "
38
+ end
39
+
40
+ def print_attr(contents, lhs, value1)
41
+ if value1.is_a?(Hash)
42
+ value1.each do |key, value2|
43
+ print_attr(contents, "#{lhs}['#{key}']", value2)
44
+ end
45
+ else
46
+ ruby_str = value1.nil? ? 'nil' : Chef::JSONCompat.to_json(value1)
47
+ contents << "#{lhs} = #{ruby_str}\n"
48
+ end
49
+ end
50
+
51
+ def print_attrs(contents, attrs, indent = 0)
52
+ return unless attrs
53
+ KnifeTopo::PRIORITIES.each do |priority|
54
+ next unless attrs[priority]
55
+ lhs = ''
56
+ indent.times { lhs << ' ' }
57
+ lhs << priority
58
+ print_attr(contents, lhs, attrs[priority])
59
+ end
60
+ end
61
+
62
+ def print_attrs_for_node(contents, n)
63
+ if n['node_type']
64
+ print_node_type_attr_header(contents, n['node_type'])
65
+ else
66
+ print_node_name_attr_header(contents, n['name'])
67
+ end
68
+ print_attrs(contents, n, 2)
69
+ contents << "end\n"
70
+ end
71
+
72
+ def print_node_type_attr_header(contents, node_type)
73
+ contents << "# Attributes for node type #{node_type}\n"
74
+ contents << "if node['topo'] && node['topo']['node_type']" \
75
+ " == '#{node_type}'\n"
76
+ end
77
+
78
+ def print_node_name_attr_header(contents, node_name)
79
+ contents << "# Attributes for node name #{node_name}\n"
80
+ contents << "if node.name == '#{node_name}'\n"
81
+ end
82
+
83
+ def cookbook_contents
84
+ contents = ''
85
+ print_attr_header(contents, @cookbook, @filename)
86
+ print_attrs(contents, @topo['attributes'])
87
+ @topo.nodes.each do |n|
88
+ print_attrs_for_node(contents, n)
89
+ end
90
+ contents
91
+ end
92
+
93
+ def copyright
94
+ Chef::Config.copyright || @cmd.config['copyright']
95
+ end
96
+ end
97
+ end
@@ -1,5 +1,4 @@
1
- module Knife
2
- module Topo
3
- VERSION = "1.1.2"
4
- end
1
+ # version
2
+ module KnifeTopo
3
+ VERSION = '2.0.1'
5
4
  end
@@ -16,99 +16,127 @@
16
16
  # limitations under the License.
17
17
  #
18
18
 
19
-
20
- require_relative 'topology_helper'
19
+ require 'chef/knife/topo/bootstrap_helper'
20
+ require 'chef/knife/topo/loader'
21
21
  require 'chef/knife/bootstrap'
22
22
 
23
- class Chef
24
- class Knife
25
- class TopoBootstrap < Chef::Knife
26
-
27
- deps do
28
- Chef::Knife::Bootstrap.load_deps
29
- end
30
-
31
- banner "knife topo bootstrap TOPOLOGY (options)"
32
-
33
- option :data_bag,
34
- :short => '-D DATA_BAG',
35
- :long => "--data-bag DATA_BAG",
36
- :description => "The data bag the topologies are stored in"
37
-
38
- option :overwrite,
39
- :long => "--overwrite",
40
- :description => "Whether to overwrite existing nodes",
41
- :boolean => true
23
+ module KnifeTopo
24
+ # knife topo bootstrap
25
+ class TopoBootstrap < Chef::Knife
26
+ deps do
27
+ require 'chef/knife/topo/processor'
28
+ end
42
29
 
43
- # Make the base bootstrap options available on topo bootstrap
44
- self.options = (Chef::Knife::Bootstrap.options).merge(self.options)
30
+ include KnifeTopo::BootstrapHelper
31
+ include KnifeTopo::Loader
32
+
33
+ banner 'knife topo bootstrap TOPOLOGY (options)'
34
+
35
+ option(
36
+ :data_bag,
37
+ short: '-D DATA_BAG',
38
+ long: '--data-bag DATA_BAG',
39
+ description: 'The data bag the topologies are stored in'
40
+ )
41
+
42
+ option(
43
+ :overwrite,
44
+ long: '--overwrite',
45
+ description: 'Whether to overwrite existing nodes',
46
+ boolean: true
47
+ )
48
+
49
+ # Make the base bootstrap options available on topo bootstrap
50
+ self.options = (Chef::Knife::Bootstrap.options).merge(TopoBootstrap.options)
51
+
52
+ attr_accessor :msgs, :results
53
+
54
+ def bootstrap_msgs
55
+ {
56
+ bootstrapped: 'Bootstrapped %{num} nodes [ %{list} ]',
57
+ skipped: 'Unexpected error',
58
+ skipped_ssh: 'Did not bootstrap %{num} nodes [ %{list} ] ' \
59
+ 'because they do not have an ssh_host',
60
+ existed: 'Did not bootstrap %{num} nodes [ %{list} ] because '\
61
+ "they already exist.\n"\
62
+ "Specify --overwrite to re-bootstrap existing nodes. \n",
63
+ failed: '%{num} nodes [ %{list} ] failed to bootstrap due to errors'
64
+ }
65
+ end
45
66
 
46
- def initialize (args)
47
- super
48
- @bootstrap_args = initialize_cmd_args(args, [ 'bootstrap', '' ])
67
+ def initialize(args)
68
+ super
69
+ @bootstrap_args = initialize_cmd_args(args, name_args, ['bootstrap', ''])
70
+ @results = {
71
+ bootstrapped: [], skipped: [], skipped_ssh: [], existed: [], failed: []
72
+ }
73
+ @msgs = bootstrap_msgs
74
+ @bootstrap = true
75
+
76
+ # All called commands need to accept union of options
77
+ Chef::Knife::Bootstrap.options = options
78
+ end
49
79
 
50
- # All called commands need to accept union of options
51
- Chef::Knife::Bootstrap.options = options
80
+ def run
81
+ validate_args
52
82
 
83
+ # load and bootstrap each node that has a ssh_host
84
+ @topo = load_topo_from_server_or_exit(@topo_name)
85
+ @processor = KnifeTopo::Processor.for_topo(@topo)
86
+ nodes = @processor.generate_nodes
87
+ nodes.each do |node_data|
88
+ node_bootstrap(node_data)
53
89
  end
54
90
 
55
- def run
56
- if !@name_args[0]
57
- show_usage
58
- ui.fatal("You must specify the name of a topology")
59
- exit 1
60
- end
61
-
62
- @bag_name = topo_bag_name(config[:data_bag])
63
- @topo_name = @name_args[0]
91
+ report
92
+ end
64
93
 
65
- # get the node names for the topology
66
- unless topo = load_from_server(@bag_name, @topo_name )
67
- ui.fatal("Topology #{@bag_name}/#{@topo_name} does not exist on the server - use 'knife topo create' first")
68
- exit(1)
69
- end
94
+ def validate_args
95
+ unless @name_args[0]
96
+ show_usage
97
+ ui.fatal('You must specify the name of a topology')
98
+ exit 1
99
+ end
100
+ @topo_name = @name_args[0]
101
+ end
70
102
 
71
- # load and bootstrap each node that has a ssh_host
72
- nodes = merge_topo_properties(topo['nodes'], topo)
73
-
74
- bootstrapped = []
75
- skipped = []
76
- existed = []
77
- failed = []
78
-
79
- if nodes.length > 0
80
- nodes.each do |node_data|
81
- node_name = node_data['name']
82
- exists = resource_exists?("nodes/#{node_name}")
83
- if(node_data['ssh_host'] && (config[:overwrite] || !exists))
84
- if run_bootstrap(node_data, @bootstrap_args, exists)
85
- bootstrapped << node_name
86
- else
87
- failed << node_name
88
- end
89
- else
90
- if(exists)
91
- existed << node_name
92
- else
93
- skipped << node_name
94
- end
95
- end
96
- end
97
- ui.info("Bootstrapped #{bootstrapped.length} nodes [ #{bootstrapped.join(', ')} ]")
98
- ui.info("Skipped #{skipped.length} nodes [ #{skipped.join(', ')} ] because they had no ssh_host information") if skipped.length > 0
99
- if existed.length > 0
100
- ui.info("Skipped #{existed.length} nodes [ #{existed.join(', ')} ] because they already exist. " +
101
- "Specify --overwrite to re-bootstrap existing nodes. " +
102
- "If you are using Chef Vault, you may need to use --bootstrap-vault options in this case.")
103
- end
104
- ui.warn("#{failed.length} nodes [ #{failed.join(', ')} ] failed to bootstrap due to errors") if failed.length > 0
103
+ # rubocop:disable Metrics/MethodLength
104
+ def node_bootstrap(node_data)
105
+ node_name = node_data['name']
106
+ state = :skipped_ssh
107
+ if node_data['ssh_host']
108
+ exists = resource_exists?("nodes/#{node_name}")
109
+ if config[:overwrite] || !exists
110
+ success = run_bootstrap(node_data, @bootstrap_args, exists)
111
+ state = success ? :bootstrapped : :failed
105
112
  else
106
- ui.info "No nodes found for topology #{display_name(topo)}"
113
+ state = :existed
107
114
  end
108
115
  end
116
+ @results[state] << node_name
117
+ success
118
+ end
119
+ # rubocop:enable Metrics/MethodLength
120
+
121
+ # Report is used by create, update and bootstrap commands
122
+ def report
123
+ if @topo['nodes'].length > 0
124
+ report_msg(:bootstrapped, :info, false) if @bootstrap
125
+ report_msg(:skipped, :info, true)
126
+ report_msg(:skipped_ssh, :info, true)
127
+ report_msg(:existed, :info, true)
128
+ report_msg(:failed, :warn, true) if @bootstrap
129
+ else
130
+ ui.info 'No nodes found'
131
+ end
132
+ ui.info("Topology: #{@topo.display_info}")
133
+ end
109
134
 
110
- include Chef::Knife::TopologyHelper
111
-
135
+ def report_msg(state, level, only_non_zero = true)
136
+ nodes = @results[state]
137
+ return if only_non_zero && nodes.length == 0
138
+ ui.send(level, @msgs[state] %
139
+ { num: nodes.length, list: nodes.join(', ') })
112
140
  end
113
141
  end
114
142
  end
@@ -16,161 +16,58 @@
16
16
  # limitations under the License.
17
17
  #
18
18
 
19
- require_relative 'topology_helper'
19
+ require 'chef/knife/topo/loader'
20
20
  require 'chef/knife/cookbook_create'
21
21
 
22
- class Chef
23
- class Knife
24
- class TopoCookbookCreate < Chef::Knife
25
-
26
- deps do
27
- Chef::Knife::CookbookCreate.load_deps
28
- end
29
-
30
- banner "knife topo cookbook create TOPOLOGY [ TOPOLOGY_FILE ] (options)"
31
-
32
- option :data_bag,
33
- :short => '-D DATA_BAG',
34
- :long => "--data-bag DATA_BAG",
35
- :description => "The data bag the topologies are stored in"
36
-
37
- # Make the base cookbook create options available on topo cookbook
38
- self.options = (Chef::Knife::CookbookCreate.options).merge(self.options)
39
-
40
- def initialize (args)
41
- super
42
- @cookbook_create_args = initialize_cmd_args(args, [ 'cookbook', 'create' ])
43
-
44
- # All called commands need to accept union of options
45
- Chef::Knife::CookbookCreate.options = options
46
-
47
- end
22
+ module KnifeTopo
23
+ # knife topo cookbook create
24
+ class TopoCookbookCreate < Chef::Knife
25
+ deps do
26
+ require 'chef/knife/topo/processor'
27
+ end
48
28
 
49
- def run
50
- if !@name_args[0]
51
- show_usage
52
- ui.fatal("You must specify the name of a topology")
53
- exit 1
54
- end
55
-
56
- @bag_name = topo_bag_name(config[:data_bag])
57
- @topo_name = @name_args[0]
58
- @topo_file = @name_args[1]
29
+ banner 'knife topo cookbook create TOPOLOGY_FILE (options)'
59
30
 
60
- # Get the topology data from either the file or the server
61
- if @topo_file
62
- topologies = load_topologies(@topo_file)
63
- index = topologies.find_index{ |t| t['name'] == @topo_name}
64
- unless index
65
- ui.fatal("Topology #{@topo_name} was not found in topology file #{@topo_file}")
66
- exit(1)
67
- end
68
- topo = topologies[index]
69
- else
70
- unless topo = load_from_server(@bag_name, @topo_name )
71
- ui.fatal("Topology #{@bag_name}/#{@topo_name} does not exist on the server - use 'knife topo create' first")
72
- exit(1)
73
- end
74
- end
31
+ option(
32
+ :data_bag,
33
+ short: '-D DATA_BAG',
34
+ long: '--data-bag DATA_BAG',
35
+ description: 'The data bag the topologies are stored in'
36
+ )
75
37
 
76
- # create the topology cookbooks
77
- attribute_cookbooks = topo['cookbook_attributes']
78
- cookbook_names = []
79
- if attribute_cookbooks && attribute_cookbooks.length > 0
80
- attribute_cookbooks.each do |cookbook_spec|
81
- cookbook_name = cookbook_spec['cookbook']
82
- run_create_cookbook(cookbook_name) unless cookbook_names.include?(cookbook_name)
83
- cookbook_names << cookbook_name
84
- create_attr_file(@cookbook_path, cookbook_name,
85
- cookbook_spec['filename'] + ".rb", cookbook_spec)
86
- end
87
- else
88
- ui.info "No topology cookbook has been specified for topology #{@topo_name}"
89
- end
90
- end
38
+ # Make the base cookbook create options available on topo cookbook
39
+ self.options = Chef::Knife::CookbookCreate.options.merge(
40
+ TopoCookbookCreate.options)
91
41
 
92
- private
42
+ include KnifeTopo::Loader
93
43
 
94
- include Chef::Knife::TopologyHelper
44
+ def initialize(args)
45
+ super
46
+ @args = args
47
+ end
95
48
 
96
- def run_create_cookbook(cookbook_name)
97
- @cookbook_create_args[2] = cookbook_name
98
- begin
99
- command = run_cmd(Chef::Knife::CookbookCreate, @cookbook_create_args)
100
- rescue Exception => e
101
- raise if Chef::Config[:verbosity] == 2
102
- ui.warn "Create of cookbook #{cookbook_name} exited with error"
103
- humanize_exception(e)
104
- end
49
+ def run
50
+ validate_args
105
51
 
106
- # Store the cookbook path for use later
107
- @cookbook_path = File.expand_path(Array(command.config[:cookbook_path]).first)
108
- @copyright = command.config[:cookbook_copyright] || "YOUR_COMPANY_NAME"
52
+ @topo = load_topo_from_file_or_exit(@topo_file)
53
+ @processor = KnifeTopo::Processor.for_topo(@topo)
54
+ do_create_artifacts
55
+ end
109
56
 
57
+ def validate_args
58
+ unless @name_args[0]
59
+ show_usage
60
+ ui.fatal('You must specify a topology JSON file')
61
+ exit 1
110
62
  end
63
+ @topo_file = @name_args[0]
64
+ end
111
65
 
112
- def create_attr_file(dir, cookbook_name, filename, attrs)
113
-
114
- ui.info("** Creating attribute file #{filename}")
115
-
116
- open(File.join(dir, cookbook_name, "attributes", filename), "w") do |file|
117
- file.puts <<-EOH
118
- #
119
- # THIS FILE IS GENERATED BY THE KNIFE TOPO PLUGIN - MANUAL CHANGES WILL BE OVERWRITTEN
120
- #
121
- # Cookbook Name:: #{cookbook_name}
122
- # Attribute File:: #{filename}
123
- #
124
- # Copyright #{Time.now.year}, #{@copyright}
125
- #
126
- EOH
127
-
128
- # Print out attribute line
129
- def print_attr(file, lhs, value1)
130
- if value1.is_a?(Hash)
131
- value1.each do |key, value2|
132
- print_attr(file, "#{lhs}['#{key}']", value2)
133
- end
134
- else
135
- rubyString = (value1 == nil) ? "nil" : Chef::JSONCompat.to_json(value1);
136
- file.write "#{lhs} = " + rubyString + " \n"
137
- end
138
- end
139
-
140
- # Print out attributes hashed by priority
141
- def print_priority_attrs(file, attrs, indent=0)
142
- %w(default force_default normal override force_override).each do |priority|
143
- if attrs[priority]
144
- lhs = ""
145
- indent.times { |i| lhs += " " }
146
- lhs += priority
147
- print_attr(file, lhs, attrs[priority])
148
- end
149
- end
150
- end
151
-
152
- # Print out qualified attributes
153
- def print_qualified_attr(file, qualifier_hash)
154
- file.puts "if node['topo'] && node['topo']['#{qualifier_hash['qualifier']}'] == \"#{qualifier_hash['value']}\""
155
- print_priority_attrs(file, qualifier_hash, 2)
156
- file.puts "end"
157
- end
158
-
159
- # Process the attributes not needing qualification
160
- print_priority_attrs(file, attrs)
161
- file.puts
162
-
163
- # Process attributes that need to be qualified
164
- if attrs['conditional']
165
- attrs['conditional'].each do |qualified_attrs|
166
- file.puts "# Attributes for specific #{qualified_attrs['qualifier']}"
167
- print_qualified_attr(file, qualified_attrs)
168
- end
169
- end
170
-
171
- end
172
- end
173
-
66
+ def do_create_artifacts
67
+ @processor.generate_artifacts(
68
+ 'cmd_args' => @args,
69
+ 'cmd' => self
70
+ )
174
71
  end
175
72
  end
176
- end
73
+ end