knife-winops 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +30 -0
  5. data/CHANGELOG.md +147 -0
  6. data/DOC_CHANGES.md +22 -0
  7. data/Gemfile +13 -0
  8. data/LICENSE +201 -0
  9. data/README.md +430 -0
  10. data/RELEASE_NOTES.md +17 -0
  11. data/Rakefile +21 -0
  12. data/appveyor.yml +36 -0
  13. data/ci.gemfile +15 -0
  14. data/knife-winops.gemspec +26 -0
  15. data/lib/chef/knife/bootstrap/Chef_bootstrap.erb +44 -0
  16. data/lib/chef/knife/bootstrap/bootstrap.ps1 +134 -0
  17. data/lib/chef/knife/bootstrap/tail.cmd +15 -0
  18. data/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +302 -0
  19. data/lib/chef/knife/bootstrap_windows_base.rb +473 -0
  20. data/lib/chef/knife/bootstrap_windows_ssh.rb +115 -0
  21. data/lib/chef/knife/bootstrap_windows_winrm.rb +102 -0
  22. data/lib/chef/knife/core/windows_bootstrap_context.rb +356 -0
  23. data/lib/chef/knife/knife_windows_base.rb +33 -0
  24. data/lib/chef/knife/windows_cert_generate.rb +155 -0
  25. data/lib/chef/knife/windows_cert_install.rb +68 -0
  26. data/lib/chef/knife/windows_helper.rb +36 -0
  27. data/lib/chef/knife/windows_listener_create.rb +107 -0
  28. data/lib/chef/knife/winrm.rb +127 -0
  29. data/lib/chef/knife/winrm_base.rb +128 -0
  30. data/lib/chef/knife/winrm_knife_base.rb +315 -0
  31. data/lib/chef/knife/winrm_session.rb +101 -0
  32. data/lib/chef/knife/winrm_shared_options.rb +54 -0
  33. data/lib/chef/knife/wsman_endpoint.rb +44 -0
  34. data/lib/chef/knife/wsman_test.rb +118 -0
  35. data/lib/knife-winops/path_helper.rb +242 -0
  36. data/lib/knife-winops/version.rb +6 -0
  37. data/spec/assets/fake_trusted_certs/excluded.txt +2 -0
  38. data/spec/assets/fake_trusted_certs/github.pem +42 -0
  39. data/spec/assets/fake_trusted_certs/google.crt +41 -0
  40. data/spec/assets/win_fake_trusted_cert_script.txt +89 -0
  41. data/spec/dummy_winrm_connection.rb +21 -0
  42. data/spec/functional/bootstrap_download_spec.rb +229 -0
  43. data/spec/spec_helper.rb +93 -0
  44. data/spec/unit/knife/bootstrap_options_spec.rb +164 -0
  45. data/spec/unit/knife/bootstrap_template_spec.rb +98 -0
  46. data/spec/unit/knife/bootstrap_windows_winrm_spec.rb +410 -0
  47. data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +292 -0
  48. data/spec/unit/knife/windows_cert_generate_spec.rb +90 -0
  49. data/spec/unit/knife/windows_cert_install_spec.rb +51 -0
  50. data/spec/unit/knife/windows_listener_create_spec.rb +76 -0
  51. data/spec/unit/knife/winrm_session_spec.rb +101 -0
  52. data/spec/unit/knife/winrm_spec.rb +494 -0
  53. data/spec/unit/knife/wsman_test_spec.rb +209 -0
  54. metadata +157 -0
@@ -0,0 +1,54 @@
1
+ #
2
+ # Original knife-windows author:: Steven Murawski (<smurawski@chef.io)
3
+ # Copyright:: Copyright (c) 2015-2016 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/knife'
20
+ require 'chef/encrypted_data_bag_item'
21
+ require 'kconv'
22
+
23
+ class Chef
24
+ class Knife
25
+ module WinrmSharedOptions
26
+
27
+ # Shared command line options for knife winrm and knife wsman test
28
+ def self.included(includer)
29
+ includer.class_eval do
30
+ option :manual,
31
+ :short => "-m",
32
+ :long => "--manual-list",
33
+ :boolean => true,
34
+ :description => "QUERY is a space separated list of servers",
35
+ :default => false
36
+
37
+ option :attribute,
38
+ :short => "-a ATTR",
39
+ :long => "--attribute ATTR",
40
+ :description => "The attribute to use for opening the connection - default is fqdn",
41
+ :default => "fqdn"
42
+
43
+ option :concurrency,
44
+ :short => "-C NUM",
45
+ :long => "--concurrency NUM",
46
+ :description => "The number of allowed concurrent connections",
47
+ :default => 1,
48
+ :proc => lambda { |o| o.to_i }
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ #
2
+ # Original knife-windows author:: Steven Murawski (<smurawski@chef.io)
3
+ # Copyright:: Copyright (c) 2015-2016 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
+ class Chef
20
+ class Knife
21
+ class WsmanEndpoint
22
+ attr_accessor :host
23
+ attr_accessor :wsman_port
24
+ attr_accessor :wsman_url
25
+ attr_accessor :product_version
26
+ attr_accessor :protocol_version
27
+ attr_accessor :product_vendor
28
+ attr_accessor :response_status_code
29
+ attr_accessor :error_message
30
+
31
+ def initialize(name, port, url)
32
+ @host = name
33
+ @wsman_port = port
34
+ @wsman_url = url
35
+ end
36
+
37
+ def to_hash
38
+ hash = {}
39
+ instance_variables.each {|var| hash[var.to_s.delete("@")] = instance_variable_get(var) }
40
+ hash
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,118 @@
1
+ #
2
+ # Original knife-windows author:: Steven Murawski (<smurawski@chef.io>)
3
+ # Copyright:: Copyright (c) 2015-2016 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 'httpclient'
20
+ require 'chef/knife'
21
+ require 'chef/knife/winrm_knife_base'
22
+ require 'chef/knife/wsman_endpoint'
23
+
24
+ class Chef
25
+ class Knife
26
+ class WsmanTest < Knife
27
+
28
+ include Chef::Knife::WinrmCommandSharedFunctions
29
+
30
+ deps do
31
+ require 'chef/search/query'
32
+ end
33
+
34
+ banner "knife wsman test QUERY (options)"
35
+
36
+ def run
37
+ # pass a dummy password to avoid prompt for password
38
+ # but it does nothing
39
+ @config[:winrm_password] = 'cute_little_kittens'
40
+
41
+ configure_session
42
+ verify_wsman_accessiblity_for_nodes
43
+ end
44
+
45
+ private
46
+
47
+ def verify_wsman_accessiblity_for_nodes
48
+ error_count = 0
49
+ @winrm_sessions.each do |item|
50
+ Chef::Log.debug("checking for WSMAN availability at #{item.endpoint}")
51
+
52
+ ssl_error = nil
53
+ begin
54
+ response = post_identity_request(item.endpoint)
55
+ ui.msg "Connected successfully to #{item.host} at #{item.endpoint}."
56
+ rescue Exception => err
57
+ end
58
+
59
+ output_object = parse_response(item, response)
60
+ output_object.error_message += "\r\nError returned from endpoint: #{err.message}" if err
61
+
62
+ unless output_object.error_message.nil?
63
+ ui.warn "Failed to connect to #{item.host} at #{item.endpoint}."
64
+ if err && err.is_a?(OpenSSL::SSL::SSLError)
65
+ ui.warn "Failure due to an issue with SSL; likely cause would be unsuccessful certificate verification."
66
+ ui.warn "Either ensure your certificate is valid or use '--winrm-ssl-verify-mode verify_none' to ignore verification failures."
67
+ end
68
+ error_count += 1
69
+ end
70
+
71
+ if config[:verbosity] >= 1
72
+ output(output_object)
73
+ end
74
+ end
75
+ if error_count > 0
76
+ ui.error "Failed to connect to #{error_count} nodes."
77
+ exit 1
78
+ end
79
+ end
80
+
81
+ def post_identity_request(endpoint)
82
+ xml = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsmid="http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.xsd"><s:Header/><s:Body><wsmid:Identify/></s:Body></s:Envelope>'
83
+ header = {
84
+ 'WSMANIDENTIFY' => 'unauthenticated',
85
+ 'Content-Type' => 'application/soap+xml; charset=UTF-8'
86
+ }
87
+
88
+ client = HTTPClient.new
89
+ Chef::HTTP::DefaultSSLPolicy.new(client.ssl_config).set_custom_certs
90
+ client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE if resolve_no_ssl_peer_verification
91
+ client.post(endpoint, xml, header)
92
+ end
93
+
94
+ def parse_response(node, response)
95
+ output_object = Chef::Knife::WsmanEndpoint.new(node.host, node.port, node.endpoint)
96
+ output_object.response_status_code = response.status_code unless response.nil?
97
+
98
+ if response.nil? || response.status_code != 200
99
+ output_object.error_message = "No valid WSMan endoint listening at #{node.endpoint}."
100
+ else
101
+ doc = REXML::Document.new(response.body)
102
+ output_object.protocol_version = search_xpath(doc, "//wsmid:ProtocolVersion")
103
+ output_object.product_version = search_xpath(doc, "//wsmid:ProductVersion")
104
+ output_object.product_vendor = search_xpath(doc, "//wsmid:ProductVendor")
105
+ if output_object.protocol_version.to_s.strip.length == 0
106
+ output_object.error_message = "Endpoint #{node.endpoint} on #{node.host} does not appear to be a WSMAN endpoint. Response body was #{response.body}"
107
+ end
108
+ end
109
+ output_object
110
+ end
111
+
112
+ def search_xpath(document, property_name)
113
+ result = REXML::XPath.match(document, property_name)
114
+ result[0].nil? ? '' : result[0].text
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,242 @@
1
+ #
2
+ # Original knife-windows author:: Bryan McLellan <btm@loftninjas.org>
3
+ # Copyright:: Copyright (c) 2014-2016 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
+ # Sourced from Chef::Util::PathHelper.
20
+ # Should be removed when Chef 11 catches up or we stop supporting Chef 11
21
+
22
+ module Knife
23
+ module Windows
24
+ class PathHelper
25
+ # Maximum characters in a standard Windows path (260 including drive letter and NUL)
26
+ WIN_MAX_PATH = 259
27
+
28
+ def self.dirname(path)
29
+ if Chef::Platform.windows?
30
+ # Find the first slash, not counting trailing slashes
31
+ end_slash = path.size
32
+ while true
33
+ slash = path.rindex(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]/, end_slash - 1)
34
+ if !slash
35
+ return end_slash == path.size ? '.' : path_separator
36
+ elsif slash == end_slash - 1
37
+ end_slash = slash
38
+ else
39
+ return path[0..slash-1]
40
+ end
41
+ end
42
+ else
43
+ ::File.dirname(path)
44
+ end
45
+ end
46
+
47
+ BACKSLASH = '\\'.freeze
48
+
49
+ def self.path_separator
50
+ if Chef::Platform.windows?
51
+ File::ALT_SEPARATOR || BACKSLASH
52
+ else
53
+ File::SEPARATOR
54
+ end
55
+ end
56
+
57
+ def self.join(*args)
58
+ args.flatten.inject do |joined_path, component|
59
+ # Joined path ends with /
60
+ joined_path = joined_path.sub(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]+$/, '')
61
+ component = component.sub(/^[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]+/, '')
62
+ joined_path += "#{path_separator}#{component}"
63
+ end
64
+ end
65
+
66
+ def self.validate_path(path)
67
+ if Chef::Platform.windows?
68
+ unless printable?(path)
69
+ msg = "Path '#{path}' contains non-printable characters. Check that backslashes are escaped with another backslash (e.g. C:\\\\Windows) in double-quoted strings."
70
+ Chef::Log.error(msg)
71
+ raise Chef::Exceptions::ValidationFailed, msg
72
+ end
73
+
74
+ if windows_max_length_exceeded?(path)
75
+ Chef::Log.debug("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'")
76
+ path.insert(0, "\\\\?\\")
77
+ end
78
+ end
79
+
80
+ path
81
+ end
82
+
83
+ def self.windows_max_length_exceeded?(path)
84
+ # Check to see if paths without the \\?\ prefix are over the maximum allowed length for the Windows API
85
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
86
+ unless path =~ /^\\\\?\\/
87
+ if path.length > WIN_MAX_PATH
88
+ return true
89
+ end
90
+ end
91
+
92
+ false
93
+ end
94
+
95
+ def self.printable?(string)
96
+ # returns true if string is free of non-printable characters (escape sequences)
97
+ # this returns false for whitespace escape sequences as well, e.g. \n\t
98
+ if string =~ /[^[:print:]]/
99
+ false
100
+ else
101
+ true
102
+ end
103
+ end
104
+
105
+ # Produces a comparable path.
106
+ def self.canonical_path(path, add_prefix=true)
107
+ # Rather than find an equivalent for File.absolute_path on 1.8.7, just bail out
108
+ raise NotImplementedError, "This feature is not supported on Ruby versions < 1.9" if RUBY_VERSION.to_f < 1.9
109
+
110
+ # First remove extra separators and resolve any relative paths
111
+ abs_path = File.absolute_path(path)
112
+
113
+ if Chef::Platform.windows?
114
+ # Add the \\?\ API prefix on Windows unless add_prefix is false
115
+ # Downcase on Windows where paths are still case-insensitive
116
+ abs_path.gsub!(::File::SEPARATOR, path_separator)
117
+ if add_prefix && abs_path !~ /^\\\\?\\/
118
+ abs_path.insert(0, "\\\\?\\")
119
+ end
120
+
121
+ abs_path.downcase!
122
+ end
123
+
124
+ abs_path
125
+ end
126
+
127
+ def self.cleanpath(path)
128
+ path = Pathname.new(path).cleanpath.to_s
129
+ # ensure all forward slashes are backslashes
130
+ if Chef::Platform.windows?
131
+ path = path.gsub(File::SEPARATOR, path_separator)
132
+ end
133
+ path
134
+ end
135
+
136
+ def self.paths_eql?(path1, path2)
137
+ canonical_path(path1) == canonical_path(path2)
138
+ end
139
+
140
+ # Note: this method is deprecated. Please use escape_glob_dirs
141
+ # Paths which may contain glob-reserved characters need
142
+ # to be escaped before globbing can be done.
143
+ # http://stackoverflow.com/questions/14127343
144
+ def self.escape_glob(*parts)
145
+ path = cleanpath(join(*parts))
146
+ path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\"+x }
147
+ end
148
+
149
+ # This function does not switch to backslashes for windows
150
+ # This is because only forwardslashes should be used with dir (even for windows)
151
+ def self.escape_glob_dir(*parts)
152
+ path = Pathname.new(join(*parts)).cleanpath.to_s
153
+ path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\" + x }
154
+ end
155
+
156
+ def self.relative_path_from(from, to)
157
+ pathname = Pathname.new(Chef::Util::PathHelper.cleanpath(to)).relative_path_from(Pathname.new(Chef::Util::PathHelper.cleanpath(from)))
158
+ end
159
+
160
+ # Retrieves the "home directory" of the current user while trying to ascertain the existence
161
+ # of said directory. The path returned uses / for all separators (the ruby standard format).
162
+ # If the home directory doesn't exist or an error is otherwise encountered, nil is returned.
163
+ #
164
+ # If a set of path elements is provided, they are appended as-is to the home path if the
165
+ # homepath exists.
166
+ #
167
+ # If an optional block is provided, the joined path is passed to that block if the home path is
168
+ # valid and the result of the block is returned instead.
169
+ #
170
+ # Home-path discovery is performed once. If a path is discovered, that value is memoized so
171
+ # that subsequent calls to home_dir don't bounce around.
172
+ #
173
+ # See self.all_homes.
174
+ def self.home(*args)
175
+ @@home_dir ||= self.all_homes { |p| break p }
176
+ if @@home_dir
177
+ path = File.join(@@home_dir, *args)
178
+ block_given? ? (yield path) : path
179
+ end
180
+ end
181
+
182
+ # See self.home. This method performs a similar operation except that it yields all the different
183
+ # possible values of 'HOME' that one could have on this platform. Hence, on windows, if
184
+ # HOMEDRIVE\HOMEPATH and USERPROFILE are different, the provided block will be called twice.
185
+ # This method goes out and checks the existence of each location at the time of the call.
186
+ #
187
+ # The return is a list of all the returned values from each block invocation or a list of paths
188
+ # if no block is provided.
189
+ def self.all_homes(*args)
190
+ paths = []
191
+ if Chef::Platform.windows?
192
+ # By default, Ruby uses the the following environment variables to determine Dir.home:
193
+ # HOME
194
+ # HOMEDRIVE HOMEPATH
195
+ # USERPROFILE
196
+ # Ruby only checks to see if the variable is specified - not if the directory actually exists.
197
+ # On Windows, HOMEDRIVE HOMEPATH can point to a different location (such as an unavailable network mounted drive)
198
+ # while USERPROFILE points to the location where the user application settings and profile are stored. HOME
199
+ # is not defined as an environment variable (usually). If the home path actually uses UNC, then the prefix is
200
+ # HOMESHARE instead of HOMEDRIVE.
201
+ #
202
+ # We instead walk down the following and only include paths that actually exist.
203
+ # HOME
204
+ # HOMEDRIVE HOMEPATH
205
+ # HOMESHARE HOMEPATH
206
+ # USERPROFILE
207
+
208
+ paths << ENV['HOME']
209
+ paths << ENV['HOMEDRIVE'] + ENV['HOMEPATH'] if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
210
+ paths << ENV['HOMESHARE'] + ENV['HOMEPATH'] if ENV['HOMESHARE'] && ENV['HOMEPATH']
211
+ paths << ENV['USERPROFILE']
212
+ end
213
+ paths << Dir.home if ENV['HOME']
214
+
215
+ # Depending on what environment variables we're using, the slashes can go in any which way.
216
+ # Just change them all to / to keep things consistent.
217
+ # Note: Maybe this is a bad idea on some unixy systems where \ might be a valid character depending on
218
+ # the particular brand of kool-aid you consume. This code assumes that \ and / are both
219
+ # path separators on any system being used.
220
+ paths = paths.map { |home_path| home_path.gsub(path_separator, ::File::SEPARATOR) if home_path }
221
+
222
+ # Filter out duplicate paths and paths that don't exist.
223
+ valid_paths = paths.select { |home_path| home_path && Dir.exists?(home_path) }
224
+ valid_paths = valid_paths.uniq
225
+
226
+ # Join all optional path elements at the end.
227
+ # If a block is provided, invoke it - otherwise just return what we've got.
228
+ joined_paths = valid_paths.map { |home_path| File.join(home_path, *args) }
229
+ if block_given?
230
+ joined_paths.each { |p| yield p }
231
+ else
232
+ joined_paths
233
+ end
234
+ end
235
+
236
+ end
237
+ end
238
+ end
239
+
240
+ # Break a require loop when require chef/util/path_helper
241
+ require 'chef/platform'
242
+ require 'chef/exceptions'