knife-windows 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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