knife-windows 0.8.6 → 1.0.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +17 -3
  4. data/CHANGELOG.md +25 -6
  5. data/DOC_CHANGES.md +323 -0
  6. data/Gemfile +2 -1
  7. data/README.md +160 -29
  8. data/RELEASE_NOTES.md +59 -6
  9. data/appveyor.yml +42 -0
  10. data/ci.gemfile +15 -0
  11. data/knife-windows.gemspec +4 -2
  12. data/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +35 -21
  13. data/lib/chef/knife/bootstrap_windows_base.rb +155 -31
  14. data/lib/chef/knife/bootstrap_windows_ssh.rb +1 -1
  15. data/lib/chef/knife/bootstrap_windows_winrm.rb +17 -10
  16. data/lib/chef/knife/core/windows_bootstrap_context.rb +67 -16
  17. data/lib/chef/knife/windows_cert_generate.rb +155 -0
  18. data/lib/chef/knife/windows_cert_install.rb +62 -0
  19. data/lib/chef/knife/windows_helper.rb +3 -1
  20. data/lib/chef/knife/windows_listener_create.rb +100 -0
  21. data/lib/chef/knife/winrm.rb +84 -208
  22. data/lib/chef/knife/winrm_base.rb +36 -10
  23. data/lib/chef/knife/winrm_knife_base.rb +201 -0
  24. data/lib/chef/knife/winrm_session.rb +72 -0
  25. data/lib/chef/knife/winrm_shared_options.rb +47 -0
  26. data/lib/chef/knife/wsman_endpoint.rb +44 -0
  27. data/lib/chef/knife/wsman_test.rb +96 -0
  28. data/lib/knife-windows/path_helper.rb +77 -0
  29. data/lib/knife-windows/version.rb +1 -1
  30. data/spec/functional/bootstrap_download_spec.rb +41 -23
  31. data/spec/spec_helper.rb +11 -1
  32. data/spec/unit/knife/bootstrap_template_spec.rb +27 -27
  33. data/spec/unit/knife/bootstrap_windows_winrm_spec.rb +67 -23
  34. data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +47 -0
  35. data/spec/unit/knife/windows_cert_generate_spec.rb +90 -0
  36. data/spec/unit/knife/windows_cert_install_spec.rb +35 -0
  37. data/spec/unit/knife/windows_listener_create_spec.rb +61 -0
  38. data/spec/unit/knife/winrm_session_spec.rb +47 -0
  39. data/spec/unit/knife/winrm_spec.rb +222 -56
  40. data/spec/unit/knife/wsman_test_spec.rb +176 -0
  41. metadata +51 -20
@@ -53,7 +53,7 @@ class Chef
53
53
  :description => "The ssh port",
54
54
  :default => "22",
55
55
  :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
56
-
56
+
57
57
  option :ssh_gateway,
58
58
  :short => "-G GATEWAY",
59
59
  :long => "--ssh-gateway GATEWAY",
@@ -19,7 +19,8 @@
19
19
  require 'chef/knife/bootstrap_windows_base'
20
20
  require 'chef/knife/winrm'
21
21
  require 'chef/knife/winrm_base'
22
- require 'chef/knife/bootstrap'
22
+ require 'chef/knife/winrm_knife_base'
23
+
23
24
 
24
25
  class Chef
25
26
  class Knife
@@ -27,6 +28,7 @@ class Chef
27
28
 
28
29
  include Chef::Knife::BootstrapWindowsBase
29
30
  include Chef::Knife::WinrmBase
31
+ include Chef::Knife::WinrmCommandSharedFunctions
30
32
 
31
33
  deps do
32
34
  require 'chef/knife/core/windows_bootstrap_context'
@@ -38,27 +40,33 @@ class Chef
38
40
  banner "knife bootstrap windows winrm FQDN (options)"
39
41
 
40
42
  def run
43
+ if (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key])))
44
+ if !negotiate_auth? && !(locate_config_value(:winrm_transport) == 'ssl')
45
+ ui.error("Validatorless bootstrap over unsecure winrm channels could expose your key to network sniffing")
46
+ exit 1
47
+ end
48
+ end
41
49
  bootstrap
42
50
  end
43
51
 
44
-
45
52
  def run_command(command = '')
46
53
  winrm = Chef::Knife::Winrm.new
47
54
  winrm.name_args = [ server_name, command ]
48
55
  winrm.config[:winrm_user] = locate_config_value(:winrm_user)
49
56
  winrm.config[:winrm_password] = locate_config_value(:winrm_password)
50
57
  winrm.config[:winrm_transport] = locate_config_value(:winrm_transport)
51
- winrm.config[:kerberos_keytab_file] = Chef::Config[:knife][:kerberos_keytab_file] if Chef::Config[:knife][:kerberos_keytab_file]
52
- winrm.config[:kerberos_realm] = Chef::Config[:knife][:kerberos_realm] if Chef::Config[:knife][:kerberos_realm]
53
- winrm.config[:kerberos_service] = Chef::Config[:knife][:kerberos_service] if Chef::Config[:knife][:kerberos_service]
54
- winrm.config[:ca_trust_file] = Chef::Config[:knife][:ca_trust_file] if Chef::Config[:knife][:ca_trust_file]
58
+ winrm.config[:winrm_ssl_verify_mode] = locate_config_value(:winrm_ssl_verify_mode)
59
+ winrm.config[:kerberos_keytab_file] = locate_config_value(:kerberos_keytab_file) if locate_config_value(:kerberos_keytab_file)
60
+ winrm.config[:kerberos_realm] = locate_config_value(:kerberos_realm) if locate_config_value(:kerberos_realm)
61
+ winrm.config[:kerberos_service] = locate_config_value(:kerberos_service) if locate_config_value(:kerberos_service)
62
+ winrm.config[:ca_trust_file] = locate_config_value(:ca_trust_file) if locate_config_value(:ca_trust_file)
55
63
  winrm.config[:manual] = true
56
64
  winrm.config[:winrm_port] = locate_config_value(:winrm_port)
57
65
  winrm.config[:suppress_auth_failure] = true
58
-
59
- #If you turn off the return flag, then winrm.run won't atually check and
66
+
67
+ #If you turn off the return flag, then winrm.run won't atually check and
60
68
  #return the error
61
- #codes. Otherwise, it ignores the return value of the server call.
69
+ #codes. Otherwise, it ignores the return value of the server call.
62
70
  winrm.config[:returns] = "0"
63
71
  winrm.run
64
72
  end
@@ -99,4 +107,3 @@ class Chef
99
107
  end
100
108
  end
101
109
  end
102
-
@@ -22,6 +22,7 @@ require 'chef/knife/core/bootstrap_context'
22
22
  require 'knife-windows/path_helper'
23
23
  # require 'chef/util/path_helper'
24
24
 
25
+
25
26
  class Chef
26
27
  class Knife
27
28
  module Core
@@ -34,27 +35,37 @@ class Chef
34
35
  class WindowsBootstrapContext < BootstrapContext
35
36
  PathHelper = ::Knife::Windows::PathHelper
36
37
 
37
- def initialize(config, run_list, chef_config)
38
+ attr_accessor :client_pem
39
+
40
+ def initialize(config, run_list, chef_config, secret=nil)
38
41
  @config = config
39
42
  @run_list = run_list
40
43
  @chef_config = chef_config
41
- super(config, run_list, chef_config)
44
+ @secret = secret
45
+ # Compatibility with Chef 12 and Chef 11 versions
46
+ begin
47
+ # Pass along the secret parameter for Chef 12
48
+ super(config, run_list, chef_config, secret)
49
+ rescue ArgumentError
50
+ # The Chef 11 base class only has parameters for initialize
51
+ super(config, run_list, chef_config)
52
+ end
42
53
  end
43
54
 
44
55
  def validation_key
45
- if super
46
- escape_and_echo(super)
56
+ if File.exist?(File.expand_path(@chef_config[:validation_key]))
57
+ IO.read(File.expand_path(@chef_config[:validation_key]))
47
58
  else
48
- raise 'Knife-Windows < 1.0 does not support validatorless bootstraps'
59
+ false
49
60
  end
50
61
  end
51
62
 
52
- def encrypted_data_bag_secret
53
- escape_and_echo(@config[:encrypted_data_bag_secret])
63
+ def secret
64
+ escape_and_echo(@config[:secret])
54
65
  end
55
66
 
56
- def trusted_certs
57
- @trusted_certs ||= trusted_certs_content
67
+ def trusted_certs_script
68
+ @trusted_certs_script ||= trusted_certs_content
58
69
  end
59
70
 
60
71
  def config_content
@@ -64,8 +75,6 @@ log_location STDOUT
64
75
 
65
76
  chef_server_url "#{@chef_config[:chef_server_url]}"
66
77
  validation_client_name "#{@chef_config[:validation_client_name]}"
67
- client_key "c:/chef/client.pem"
68
- validation_key "c:/chef/validation.pem"
69
78
 
70
79
  file_cache_path "c:/chef/cache"
71
80
  file_backup_path "c:/chef/backup"
@@ -119,12 +128,12 @@ CONFIG
119
128
  client_rb << %Q{no_proxy "#{knife_config[:bootstrap_no_proxy]}"\n}
120
129
  end
121
130
 
122
- if @config[:encrypted_data_bag_secret]
131
+ if @config[:secret]
123
132
  client_rb << %Q{encrypted_data_bag_secret "c:/chef/encrypted_data_bag_secret"\n}
124
133
  end
125
134
 
126
- unless trusted_certs.empty?
127
- client_rb << %Q{trusted_certs_dir "C:/chef/trusted_certs"\n}
135
+ unless trusted_certs_script.empty?
136
+ client_rb << %Q{trusted_certs_dir "c:/chef/trusted_certs"\n}
128
137
  end
129
138
 
130
139
  escape_and_echo(client_rb)
@@ -158,10 +167,30 @@ CONFIG
158
167
  end
159
168
 
160
169
  def win_wget
170
+ # I tried my best to figure out how to properly url decode and switch / to \
171
+ # but this is VBScript - so I don't really care that badly.
161
172
  win_wget = <<-WGET
162
173
  url = WScript.Arguments.Named("url")
163
174
  path = WScript.Arguments.Named("path")
164
175
  proxy = null
176
+ '* Vaguely attempt to handle file:// scheme urls by url unescaping and switching all
177
+ '* / into \. Also assume that file:/// is a local absolute path and that file://<foo>
178
+ '* is possibly a network file path.
179
+ If InStr(url, "file://") = 1 Then
180
+ url = Unescape(url)
181
+ If InStr(url, "file:///") = 1 Then
182
+ sourcePath = Mid(url, Len("file:///") + 1)
183
+ Else
184
+ sourcePath = Mid(url, Len("file:") + 1)
185
+ End If
186
+ sourcePath = Replace(sourcePath, "/", "\\")
187
+
188
+ Set objFSO = CreateObject("Scripting.FileSystemObject")
189
+ If objFSO.Fileexists(path) Then objFSO.DeleteFile path
190
+ objFSO.CopyFile sourcePath, path, true
191
+ Set objFSO = Nothing
192
+
193
+ Else
165
194
  Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP")
166
195
  Set wshShell = CreateObject( "WScript.Shell" )
167
196
  Set objUserVariables = wshShell.Environment("USER")
@@ -200,8 +229,9 @@ Set objFSO = Nothing
200
229
  objADOStream.SaveToFile path
201
230
  objADOStream.Close
202
231
  Set objADOStream = Nothing
203
- End if
232
+ End If
204
233
  Set objXMLHTTP = Nothing
234
+ End If
205
235
  WGET
206
236
  escape_and_echo(win_wget)
207
237
  end
@@ -235,6 +265,21 @@ WGET_PS
235
265
  local_download_path = "%TEMP%\\chef-client-latest.msi"
236
266
  end
237
267
 
268
+ def msi_url(machine_os=nil, machine_arch=nil, download_context=nil)
269
+ # The default msi path has a number of url query parameters - we attempt to substitute
270
+ # such parameters in as long as they are provided by the template.
271
+
272
+ if @config[:msi_url].nil? || @config[:msi_url].empty?
273
+ url = "https://www.chef.io/chef/download?p=windows"
274
+ url += "&pv=#{machine_os}" unless machine_os.nil?
275
+ url += "&m=#{machine_arch}" unless machine_arch.nil?
276
+ url += "&DownloadContext=#{download_context}" unless download_context.nil?
277
+ url += latest_current_windows_chef_version_query
278
+ else
279
+ @config[:msi_url]
280
+ end
281
+ end
282
+
238
283
  def first_boot
239
284
  first_boot_attributes_and_run_list = (@config[:first_boot_attributes] || {}).merge(:run_list => @run_list)
240
285
  escape_and_echo(first_boot_attributes_and_run_list.to_json)
@@ -250,9 +295,15 @@ WGET_PS
250
295
  private
251
296
 
252
297
  def install_command(executor_quote)
253
- "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
298
+ if @config[:install_as_service]
299
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote} ADDLOCAL=#{executor_quote}ChefClientFeature,ChefServiceFeature#{executor_quote}"
300
+ else
301
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
302
+ end
254
303
  end
255
304
 
305
+ # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped
306
+ # This string should contain both the commands necessary to both create the files, as well as their content
256
307
  def trusted_certs_content
257
308
  content = ""
258
309
  if @chef_config[:trusted_certs_dir]
@@ -0,0 +1,155 @@
1
+ # Author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
2
+ # Copyright:: Copyright (c) 2014 Opscode, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'chef/knife'
19
+ require 'chef/knife/winrm_base'
20
+ require 'openssl'
21
+ require 'socket'
22
+
23
+ class Chef
24
+ class Knife
25
+ class WindowsCertGenerate < Knife
26
+
27
+ attr_accessor :thumbprint, :hostname
28
+
29
+ banner "knife windows cert generate FILE_PATH (options)"
30
+
31
+ option :hostname,
32
+ :short => "-H HOSTNAME",
33
+ :long => "--hostname HOSTNAME",
34
+ :description => "Use to specify the hostname for the listener.
35
+ For example, --hostname something.mydomain.com or *.mydomain.com.",
36
+ :required => true
37
+
38
+ option :output_file,
39
+ :short => "-o PATH",
40
+ :long => "--output-file PATH",
41
+ :description => "Specifies the file path at which to generate the 3 certificate files of type .pfx, .b64, and .pem. The default is './winrmcert'.",
42
+ :default => "winrmcert"
43
+
44
+ option :key_length,
45
+ :short => "-k LENGTH",
46
+ :long => "--key-length LENGTH",
47
+ :description => "Default is 2048",
48
+ :default => "2048"
49
+
50
+ option :cert_validity,
51
+ :short => "-cv MONTHS",
52
+ :long => "--cert-validity MONTHS",
53
+ :description => "Default is 24 months",
54
+ :default => "24"
55
+
56
+ option :cert_passphrase,
57
+ :short => "-cp PASSWORD",
58
+ :long => "--cert-passphrase PASSWORD",
59
+ :description => "Password for certificate."
60
+
61
+ def generate_keypair
62
+ OpenSSL::PKey::RSA.new(config[:key_length].to_i)
63
+ end
64
+
65
+ def prompt_for_passphrase
66
+ passphrase = ""
67
+ begin
68
+ print "Passphrases do not match. Try again.\n" unless passphrase.empty?
69
+ print "Enter certificate passphrase (empty for no passphrase):"
70
+ passphrase = STDIN.gets
71
+ return passphrase.strip if passphrase == "\n"
72
+ print "Enter same passphrase again:"
73
+ confirm_passphrase = STDIN.gets
74
+ end until passphrase == confirm_passphrase
75
+ passphrase.strip
76
+ end
77
+
78
+ def generate_certificate rsa_key
79
+ @hostname = config[:hostname] if config[:hostname]
80
+
81
+ #Create a self-signed X509 certificate from the rsa_key (unencrypted)
82
+ cert = OpenSSL::X509::Certificate.new
83
+ cert.version = 2
84
+ cert.serial = Random.rand(65534) + 1 # 2 digit byte range random number for better security aspect
85
+
86
+ cert.subject = OpenSSL::X509::Name.parse "/CN=#{@hostname}"
87
+ cert.issuer = cert.subject
88
+ cert.public_key = rsa_key.public_key
89
+ cert.not_before = Time.now
90
+ cert.not_after = cert.not_before + 2 * 365 * config[:cert_validity].to_i * 60 * 60 # 2 years validity
91
+ ef = OpenSSL::X509::ExtensionFactory.new
92
+ ef.subject_certificate = cert
93
+ ef.issuer_certificate = cert
94
+ cert.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false))
95
+ cert.add_extension(ef.create_extension("authorityKeyIdentifier","keyid:always",false))
96
+ cert.add_extension(ef.create_extension("extendedKeyUsage", "1.3.6.1.5.5.7.3.1", false))
97
+ cert.sign(rsa_key, OpenSSL::Digest::SHA1.new)
98
+ @thumbprint = OpenSSL::Digest::SHA1.new(cert.to_der)
99
+ cert
100
+ end
101
+
102
+ def write_certificate_to_file(cert, file_path, rsa_key)
103
+ File.open(file_path + ".pem", "wb") { |f| f.print cert.to_pem }
104
+ config[:cert_passphrase] = prompt_for_passphrase unless config[:cert_passphrase]
105
+ pfx = OpenSSL::PKCS12.create("#{config[:cert_passphrase]}", "winrmcert", rsa_key, cert)
106
+ File.open(file_path + ".pfx", "wb") { |f| f.print pfx.to_der }
107
+ File.open(file_path + ".b64", "wb") { |f| f.print Base64.strict_encode64(pfx.to_der) }
108
+ end
109
+
110
+ def certificates_already_exist?(file_path)
111
+ certs_exists = false
112
+ %w{pem pfx b64}.each do |extn|
113
+ if !Dir.glob("#{file_path}.*#{extn}").empty?
114
+ certs_exists = true
115
+ break
116
+ end
117
+ end
118
+
119
+ if certs_exists
120
+ begin
121
+ confirm("Do you really want to overwrite existing certificates")
122
+ rescue SystemExit # Need to handle this as confirming with N/n raises SystemExit exception
123
+ exit!
124
+ end
125
+ end
126
+ end
127
+
128
+ def run
129
+ STDOUT.sync = STDERR.sync = true
130
+
131
+ # takes user specified first cli value as a destination file path for generated cert.
132
+ file_path = @name_args.empty? ? config[:output_file].sub(/\.(\w+)$/,'') : @name_args.first
133
+
134
+ # check if certs already exists at given file path
135
+ certificates_already_exist? file_path
136
+
137
+ begin
138
+ filename = File.basename(file_path)
139
+ rsa_key = generate_keypair
140
+ cert = generate_certificate rsa_key
141
+ write_certificate_to_file cert, file_path, rsa_key
142
+ ui.info "Generated Certificates:"
143
+ ui.info "- #{filename}.pfx - PKCS12 format key pair. Contains public and private keys, can be used with an SSL server."
144
+ ui.info "- #{filename}.b64 - Base64 encoded PKCS12 key pair. Contains public and private keys, used by some cloud provider API's to configure SSL servers."
145
+ ui.info "- #{filename}.pem - Base64 encoded public certificate only. Required by the client to connect to the server."
146
+ ui.info "Certificate Thumbprint: #{@thumbprint.to_s.upcase}"
147
+ rescue => e
148
+ puts "ERROR: + #{e}"
149
+ end
150
+ end
151
+
152
+ end
153
+ end
154
+ end
155
+
@@ -0,0 +1,62 @@
1
+ # Author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
2
+ # Copyright:: Copyright (c) 2014 Opscode, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'chef/knife'
19
+ require 'chef/knife/winrm_base'
20
+
21
+ class Chef
22
+ class Knife
23
+ class WindowsCertInstall < Knife
24
+
25
+ banner "knife windows cert install CERT [CERT] (options)"
26
+
27
+ option :cert_passphrase,
28
+ :short => "-cp PASSWORD",
29
+ :long => "--cert-passphrase PASSWORD",
30
+ :description => "Password for certificate."
31
+
32
+ def get_cert_passphrase
33
+ print "Enter given certificate's passphrase (empty for no passphrase):"
34
+ passphrase = STDIN.gets
35
+ passphrase.strip
36
+ end
37
+
38
+ def run
39
+ STDOUT.sync = STDERR.sync = true
40
+ if @name_args.empty?
41
+ ui.error "Please specify the certificate path. e.g- 'knife windows cert install <path>"
42
+ exit 1
43
+ end
44
+ file_path = @name_args.first
45
+ config[:cert_passphrase] = get_cert_passphrase unless config[:cert_passphrase]
46
+
47
+ begin
48
+ ui.info "Adding certificate to the Windows Certificate Store..."
49
+ result = %x{powershell.exe -Command " '#{config[:cert_passphrase]}' | certutil -importPFX '#{file_path}' AT_KEYEXCHANGE"}
50
+ if $?.exitstatus == 0
51
+ ui.info "Certificate added to Certificate Store"
52
+ else
53
+ ui.info "Error adding the certificate. Use -VV option for details"
54
+ end
55
+ Chef::Log.debug "#{result}"
56
+ rescue => e
57
+ puts "ERROR: + #{e}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end