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,33 @@
1
+ #
2
+ # Original knife-windows author:: Aliasgar Batterywala (<aliasgar.batterywala@clogeny.com>)
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
+ 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
+
@@ -0,0 +1,155 @@
1
+ # Original knife-windows author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
2
+ # Copyright:: Copyright (c) 2014-2016 Chef Software, 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,68 @@
1
+ # Original knife-windows author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
2
+ # Copyright:: Copyright (c) 2014-2016 Chef Software, 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
@@ -0,0 +1,36 @@
1
+ #
2
+ # Original knife-windows author:: Chirag Jog (<chirag@clogeny.com>)
3
+ # Copyright:: Copyright (c) 2013-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/knife/winrm'
21
+ require 'chef/knife/bootstrap_windows_ssh'
22
+ require 'chef/knife/bootstrap_windows_winrm'
23
+ require 'chef/knife/wsman_test'
24
+
25
+ class Chef
26
+ class Knife
27
+ class WindowsHelper < Knife
28
+
29
+ banner "#{BootstrapWindowsWinrm.banner}\n" +
30
+ "#{BootstrapWindowsSsh.banner}\n" +
31
+ "#{Winrm.banner}\n" +
32
+ "#{WsmanTest.banner}"
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,107 @@
1
+ # Original knife-windows author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
2
+ # Copyright:: Copyright (c) 2014-2016 Chef Software, 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
+
22
+ class Chef
23
+ class Knife
24
+ class WindowsListenerCreate < Knife
25
+
26
+ banner "knife windows listener create (options)"
27
+
28
+ option :cert_install,
29
+ :short => "-c CERT_PATH",
30
+ :long => "--cert-install CERT_PATH",
31
+ :description => "Adds specified certificate to the Windows Certificate Store's Local Machine personal store before creating listener."
32
+
33
+ option :port,
34
+ :short => "-p PORT",
35
+ :long => "--port PORT",
36
+ :description => "Specify port. Default is 5986",
37
+ :default => "5986"
38
+
39
+ option :hostname,
40
+ :short => "-h HOSTNAME",
41
+ :long => "--hostname HOSTNAME",
42
+ :description => "Hostname on the listener. Default is blank",
43
+ :default => ""
44
+
45
+ option :cert_thumbprint,
46
+ :short => "-t THUMBPRINT",
47
+ :long => "--cert-thumbprint THUMBPRINT",
48
+ :description => "Thumbprint of the certificate. Required only if --cert-install option is not used."
49
+
50
+ option :cert_passphrase,
51
+ :short => "-cp PASSWORD",
52
+ :long => "--cert-passphrase PASSWORD",
53
+ :description => "Password for certificate."
54
+
55
+ def get_cert_passphrase
56
+ print "Enter given certificate's passphrase (empty for no passphrase):"
57
+ passphrase = STDIN.gets
58
+ passphrase.strip
59
+ end
60
+
61
+ def run
62
+ STDOUT.sync = STDERR.sync = true
63
+
64
+ if Chef::Platform.windows?
65
+ begin
66
+ if config[:cert_install]
67
+ config[:cert_passphrase] = get_cert_passphrase unless config[:cert_passphrase]
68
+ result = %x{powershell.exe -Command " '#{config[:cert_passphrase]}' | certutil -importPFX '#{config[:cert_install]}' AT_KEYEXCHANGE"}
69
+ if $?.exitstatus
70
+ ui.info "Certificate installed to Certificate Store"
71
+ result = %x{powershell.exe -Command " echo (Get-PfxCertificate #{config[:cert_install]}).thumbprint "}
72
+ ui.info "Certificate Thumbprint: #{result}"
73
+ config[:cert_thumbprint] = result.strip
74
+ else
75
+ ui.error "Error installing certificate to Certificate Store"
76
+ ui.error result
77
+ exit 1
78
+ end
79
+ end
80
+
81
+ unless config[:cert_thumbprint]
82
+ ui.error "Please specify the --cert-thumbprint"
83
+ exit 1
84
+ end
85
+
86
+ result = %x{winrm create winrm/config/Listener?Address=*+Transport=HTTPS @{Hostname="#{config[:hostname]}";CertificateThumbprint="#{config[:cert_thumbprint]}";Port="#{config[:port]}"}}
87
+ Chef::Log.debug result
88
+
89
+ if ($?.exitstatus == 0)
90
+ ui.info "WinRM listener created with Port: #{config[:port]} and CertificateThumbprint: #{config[:cert_thumbprint]}"
91
+ else
92
+ ui.error "Error creating WinRM listener. use -VV for more details."
93
+ exit 1
94
+ end
95
+
96
+ rescue => e
97
+ puts "ERROR: + #{e}"
98
+ end
99
+ else
100
+ ui.error "WinRM listener can be created on Windows system only"
101
+ exit 1
102
+ end
103
+ end
104
+
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,127 @@
1
+ #
2
+ # Original knife-windows author:: Seth Chisamore (<schisamo@chef.io>)
3
+ # Copyright:: Copyright (c) 2011-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/knife/winrm_knife_base'
21
+ require 'chef/knife/windows_cert_generate'
22
+ require 'chef/knife/windows_cert_install'
23
+ require 'chef/knife/windows_listener_create'
24
+ require 'chef/knife/winrm_session'
25
+ require 'chef/knife/knife_windows_base'
26
+
27
+ class Chef
28
+ class Knife
29
+ class Winrm < Knife
30
+
31
+ include Chef::Knife::WinrmCommandSharedFunctions
32
+ include Chef::Knife::KnifeWindowsBase
33
+
34
+ deps do
35
+ require 'readline'
36
+ require 'chef/search/query'
37
+ end
38
+
39
+ attr_writer :password
40
+
41
+ banner "knife winrm QUERY COMMAND (options)"
42
+
43
+ option :returns,
44
+ :long => "--returns CODES",
45
+ :description => "A comma delimited list of return codes which indicate success",
46
+ :default => "0"
47
+
48
+ def run
49
+ STDOUT.sync = STDERR.sync = true
50
+
51
+ configure_session
52
+ exit_status = execute_remote_command
53
+ if exit_status != 0
54
+ exit exit_status
55
+ else
56
+ exit_status
57
+ end
58
+ end
59
+
60
+ def execute_remote_command
61
+ case @name_args[1]
62
+ when "interactive"
63
+ interactive
64
+ else
65
+ run_command(@name_args[1..-1].join(" "))
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def interactive
72
+ puts "WARN: Deprecated functionality. This will not be supported in future knife-winops releases."
73
+ puts "Connected to #{ui.list(session.servers.collect { |s| ui.color(s.host, :cyan) }, :inline, " and ")}"
74
+ puts
75
+ puts "To run a command on a list of servers, do:"
76
+ puts " on SERVER1 SERVER2 SERVER3; COMMAND"
77
+ puts " Example: on latte foamy; echo foobar"
78
+ puts
79
+ puts "To exit interactive mode, use 'quit!'"
80
+ puts
81
+ while 1
82
+ command = read_line
83
+ case command
84
+ when 'quit!'
85
+ puts 'Bye!'
86
+ break
87
+ when /^on (.+?); (.+)$/
88
+ raw_list = $1.split(" ")
89
+ server_list = Array.new
90
+ @winrm_sessions.each do |session_server|
91
+ server_list << session_server if raw_list.include?(session_server.host)
92
+ end
93
+ command = $2
94
+ relay_winrm_command(command, server_list)
95
+ else
96
+ relay_winrm_command(command)
97
+ end
98
+ end
99
+ end
100
+
101
+ # Present the prompt and read a single line from the console. It also
102
+ # detects ^D and returns "exit" in that case. Adds the input to the
103
+ # history, unless the input is empty. Loops repeatedly until a non-empty
104
+ # line is input.
105
+ def read_line
106
+ loop do
107
+ command = reader.readline("#{ui.color('knife-winrm>', :bold)} ", true)
108
+
109
+ if command.nil?
110
+ command = "exit"
111
+ puts(command)
112
+ else
113
+ command.strip!
114
+ end
115
+
116
+ unless command.empty?
117
+ return command
118
+ end
119
+ end
120
+ end
121
+
122
+ def reader
123
+ Readline
124
+ end
125
+ end
126
+ end
127
+ end