knife-windows 1.1.0 → 1.1.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -5
  3. data/.travis.yml +20 -20
  4. data/CHANGELOG.md +87 -83
  5. data/DOC_CHANGES.md +20 -20
  6. data/Gemfile +12 -12
  7. data/LICENSE +201 -201
  8. data/README.md +396 -396
  9. data/RELEASE_NOTES.md +34 -34
  10. data/Rakefile +21 -21
  11. data/appveyor.yml +42 -42
  12. data/ci.gemfile +15 -15
  13. data/features/knife_help.feature +20 -20
  14. data/features/support/env.rb +5 -5
  15. data/knife-windows.gemspec +28 -28
  16. data/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +247 -247
  17. data/lib/chef/knife/bootstrap_windows_base.rb +407 -401
  18. data/lib/chef/knife/bootstrap_windows_ssh.rb +110 -110
  19. data/lib/chef/knife/bootstrap_windows_winrm.rb +95 -102
  20. data/lib/chef/knife/core/windows_bootstrap_context.rb +362 -362
  21. data/lib/chef/knife/knife_windows_base.rb +33 -33
  22. data/lib/chef/knife/windows_cert_generate.rb +155 -155
  23. data/lib/chef/knife/windows_cert_install.rb +68 -68
  24. data/lib/chef/knife/windows_helper.rb +36 -36
  25. data/lib/chef/knife/windows_listener_create.rb +107 -107
  26. data/lib/chef/knife/winrm.rb +122 -212
  27. data/lib/chef/knife/winrm_base.rb +118 -118
  28. data/lib/chef/knife/winrm_knife_base.rb +309 -218
  29. data/lib/chef/knife/winrm_session.rb +82 -82
  30. data/lib/chef/knife/winrm_shared_options.rb +47 -47
  31. data/lib/chef/knife/wsman_endpoint.rb +44 -44
  32. data/lib/chef/knife/wsman_test.rb +95 -95
  33. data/lib/knife-windows/path_helper.rb +234 -234
  34. data/lib/knife-windows/version.rb +6 -6
  35. data/spec/assets/win_template_rendered_with_bootstrap_install_command.txt +217 -217
  36. data/spec/assets/win_template_rendered_with_bootstrap_install_command_on_12_5_client.txt +217 -217
  37. data/spec/assets/win_template_rendered_without_bootstrap_install_command.txt +329 -329
  38. data/spec/assets/win_template_rendered_without_bootstrap_install_command_on_12_5_client.txt +329 -329
  39. data/spec/assets/win_template_unrendered.txt +246 -246
  40. data/spec/functional/bootstrap_download_spec.rb +234 -233
  41. data/spec/spec_helper.rb +88 -88
  42. data/spec/unit/knife/bootstrap_options_spec.rb +148 -146
  43. data/spec/unit/knife/bootstrap_template_spec.rb +92 -92
  44. data/spec/unit/knife/bootstrap_windows_winrm_spec.rb +259 -243
  45. data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +151 -151
  46. data/spec/unit/knife/windows_cert_generate_spec.rb +90 -90
  47. data/spec/unit/knife/windows_cert_install_spec.rb +51 -51
  48. data/spec/unit/knife/windows_listener_create_spec.rb +76 -76
  49. data/spec/unit/knife/winrm_session_spec.rb +73 -73
  50. data/spec/unit/knife/winrm_spec.rb +551 -504
  51. data/spec/unit/knife/wsman_test_spec.rb +178 -175
  52. metadata +3 -23
@@ -1,33 +1,33 @@
1
- #
2
- # Author:: Aliasgar Batterywala (<aliasgar.batterywala@clogeny.com>)
3
- # Copyright:: Copyright (c) 2015 Opscode, 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
- module KnifeWindowsBase
22
-
23
- def locate_config_value(key)
24
- key = key.to_sym
25
- value = config[key] || Chef::Config[:knife][key] || default_config[key]
26
- Chef::Log.debug("Looking for key #{key} and found value #{value}")
27
- value
28
- end
29
-
30
- end
31
- end
32
- end
33
-
1
+ #
2
+ # Author:: Aliasgar Batterywala (<aliasgar.batterywala@clogeny.com>)
3
+ # Copyright:: Copyright (c) 2015 Opscode, 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
+ module KnifeWindowsBase
22
+
23
+ def locate_config_value(key)
24
+ key = key.to_sym
25
+ value = config[key] || Chef::Config[:knife][key] || default_config[key]
26
+ Chef::Log.debug("Looking for key #{key} and found value #{value}")
27
+ value
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+
@@ -1,155 +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
-
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
+
@@ -1,68 +1,68 @@
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
-
41
- if Chef::Platform.windows?
42
- if @name_args.empty?
43
- ui.error "Please specify the certificate path. e.g- 'knife windows cert install <path>"
44
- exit 1
45
- end
46
- file_path = @name_args.first
47
- config[:cert_passphrase] = get_cert_passphrase unless config[:cert_passphrase]
48
-
49
- begin
50
- ui.info "Adding certificate to the Windows Certificate Store..."
51
- result = %x{powershell.exe -Command " '#{config[:cert_passphrase]}' | certutil -importPFX '#{file_path}' AT_KEYEXCHANGE"}
52
- if $?.exitstatus == 0
53
- ui.info "Certificate added to Certificate Store"
54
- else
55
- ui.info "Error adding the certificate. Use -VV option for details"
56
- end
57
- Chef::Log.debug "#{result}"
58
- rescue => e
59
- puts "ERROR: + #{e}"
60
- end
61
- else
62
- ui.error "Certificate can be installed on Windows system only"
63
- exit 1
64
- end
65
- end
66
- end
67
- end
68
- end
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
+
41
+ if Chef::Platform.windows?
42
+ if @name_args.empty?
43
+ ui.error "Please specify the certificate path. e.g- 'knife windows cert install <path>"
44
+ exit 1
45
+ end
46
+ file_path = @name_args.first
47
+ config[:cert_passphrase] = get_cert_passphrase unless config[:cert_passphrase]
48
+
49
+ begin
50
+ ui.info "Adding certificate to the Windows Certificate Store..."
51
+ result = %x{powershell.exe -Command " '#{config[:cert_passphrase]}' | certutil -importPFX '#{file_path}' AT_KEYEXCHANGE"}
52
+ if $?.exitstatus == 0
53
+ ui.info "Certificate added to Certificate Store"
54
+ else
55
+ ui.info "Error adding the certificate. Use -VV option for details"
56
+ end
57
+ Chef::Log.debug "#{result}"
58
+ rescue => e
59
+ puts "ERROR: + #{e}"
60
+ end
61
+ else
62
+ ui.error "Certificate can be installed on Windows system only"
63
+ exit 1
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end