lab 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/config/test_lab.yml +61 -0
  2. data/lab.gemspec +4 -1
  3. data/lib/lab/controller/vsphere_controller.rb +2 -46
  4. data/lib/lab/driver/vsphere_driver.rb +2 -2
  5. data/lib/lab/version.rb +1 -1
  6. data/lib/lab/vm_controller.rb +15 -4
  7. data/util/console.rb +10 -0
  8. metadata +21 -43
  9. data/src/Gemfile +0 -4
  10. data/src/Rakefile +0 -1
  11. data/src/TODO +0 -15
  12. data/src/config/test_lab.yml +0 -11
  13. data/src/config/test_targets.yml +0 -21
  14. data/src/lab.gemspec +0 -35
  15. data/src/lib/lab/controller/dynagen_controller.rb +0 -14
  16. data/src/lib/lab/controller/fog_controller.rb +0 -6
  17. data/src/lib/lab/controller/remote_esxi_controller.rb +0 -62
  18. data/src/lib/lab/controller/remote_workstation_controller.rb +0 -22
  19. data/src/lib/lab/controller/virtualbox_controller.rb +0 -25
  20. data/src/lib/lab/controller/vsphere_controller.rb +0 -18
  21. data/src/lib/lab/controller/workstation_controller.rb +0 -17
  22. data/src/lib/lab/controller/workstation_vixr_controller.rb +0 -19
  23. data/src/lib/lab/controllers.rb +0 -9
  24. data/src/lib/lab/driver/dynagen_driver.rb +0 -47
  25. data/src/lib/lab/driver/fog_driver.rb +0 -104
  26. data/src/lib/lab/driver/remote_esxi_driver.rb +0 -177
  27. data/src/lib/lab/driver/remote_workstation_driver.rb +0 -197
  28. data/src/lib/lab/driver/virtualbox_driver.rb +0 -142
  29. data/src/lib/lab/driver/vm_driver.rb +0 -195
  30. data/src/lib/lab/driver/vsphere_driver.rb +0 -120
  31. data/src/lib/lab/driver/workstation_driver.rb +0 -234
  32. data/src/lib/lab/driver/workstation_vixr_driver.rb +0 -126
  33. data/src/lib/lab/drivers.rb +0 -9
  34. data/src/lib/lab/modifier/backtrack5_modifier.rb +0 -16
  35. data/src/lib/lab/modifier/dos_modifier.rb +0 -14
  36. data/src/lib/lab/modifier/test_modifier.rb +0 -16
  37. data/src/lib/lab/modifiers.rb +0 -3
  38. data/src/lib/lab/version.rb +0 -3
  39. data/src/lib/lab/vm.rb +0 -269
  40. data/src/lib/lab/vm_controller.rb +0 -275
  41. data/src/lib/lab.rb +0 -2
  42. data/src/test/.gitkeep +0 -0
@@ -1,195 +0,0 @@
1
- ##
2
- ## $Id$
3
- ##
4
-
5
- #
6
- # !!WARNING!! - All drivers are expected to filter input before running
7
- # anything based on it. This is particularly important in the case
8
- # of the drivers which wrap a command line to provide functionality.
9
- #
10
-
11
- module Lab
12
- module Drivers
13
- class VmDriver
14
-
15
- attr_accessor :vmid
16
- attr_accessor :location
17
- attr_accessor :os
18
- attr_accessor :tools
19
- attr_accessor :credentials
20
-
21
- def initialize(config)
22
-
23
- @vmid = filter_command(config["vmid"].to_s)
24
- @location = filter_command(config["location"])
25
- @credentials = config["credentials"] || []
26
- @tools = filter_input(config["tools"])
27
- @arch = filter_input(config["arch"])
28
- @os = filter_input(config["os"])
29
- @hostname = filter_input(config["hostname"]) || filter_input(config["vmid"].to_s)
30
-
31
- # Currently only implemented for the first set
32
- if @credentials.count > 0
33
- @vm_user = filter_input(@credentials[0]['user'])
34
- @vm_pass = filter_input(@credentials[0]['pass'])
35
- @vm_keyfile = filter_input(@credentials[0]['keyfile'])
36
- end
37
- end
38
-
39
- ## This interface must be implemented in a child driver class
40
- ## #########################################################
41
-
42
- def register
43
- raise "Command not Implemented"
44
- end
45
-
46
- def unregister
47
- raise "Command not Implemented"
48
- end
49
-
50
- def start
51
- raise "Command not Implemented"
52
- end
53
-
54
- def stop
55
- raise "Command not Implemented"
56
- end
57
-
58
- def suspend
59
- raise "Command not Implemented"
60
- end
61
-
62
- def pause
63
- raise "Command not Implemented"
64
- end
65
-
66
- def resume
67
- raise "Command not Implemented"
68
- end
69
-
70
- def reset
71
- raise "Command not Implemented"
72
- end
73
-
74
- def create_snapshot(snapshot)
75
- raise "Command not Implemented"
76
- end
77
-
78
- def revert_snapshot(snapshot)
79
- raise "Command not Implemented"
80
- end
81
-
82
- def delete_snapshot(snapshot)
83
- raise "Command not Implemented"
84
- end
85
-
86
- def run_command(command)
87
- raise "Command not Implemented"
88
- end
89
-
90
- def copy_from(from, to)
91
- raise "Command not Implemented"
92
- end
93
-
94
- def copy_to(from, to)
95
- raise "Command not Implemented"
96
- end
97
-
98
- def check_file_exists(file)
99
- raise "Command not Implemented"
100
- end
101
-
102
- def create_directory(directory)
103
- raise "Command not Implemented"
104
- end
105
-
106
- def cleanup
107
- raise "Command not Implemented"
108
- end
109
-
110
- ## End Interface
111
- ## #########################################################
112
-
113
- private
114
-
115
- # The only reason we don't filter here is because we need
116
- # the ability to still run clean (controlled entirely by us)
117
- # command lines.
118
- def system_command(command)
119
- puts "DEBUG: system command #{command}"
120
- system(command)
121
- end
122
-
123
- def remote_system_command(command)
124
- puts "DEBUG: remote system command #{command} running with user #{@user}@#{@host}"
125
- system_command("ssh #{@user}@#{@host} \"#{command}\"")
126
- end
127
-
128
- def scp_to(local,remote)
129
- if @vm_keyfile
130
- #puts "DEBUG: authenticating to #{@hostname} as #{@vm_user} with key #{@vm_keyfile}"
131
- Net::SCP.start(@hostname, @vm_user, :keys => [@vm_keyfile]) do |scp|
132
- puts "DEBUG: uploading #{local} to #{remote}"
133
- scp.upload!(local,remote)
134
- end
135
- else
136
- Net::SCP.start(@hostname, @vm_user, :password => @vm_pass, :auth_methods => ["password"]) do |scp|
137
- puts "DEBUG: uploading #{local} to #{remote}"
138
- scp.upload!(local,remote)
139
- end
140
- end
141
- end
142
-
143
- def scp_from(remote, local)
144
- # download a file from a remote server
145
- if @vm_keyfile
146
- #puts "DEBUG: authenticating to #{@hostname} as #{@vm_user} with key #{@vm_keyfile}"
147
- Net::SCP.start(@hostname, @vm_user, :keys => [@vm_keyfile]) do |scp|
148
- puts "DEBUG: downloading #{remote} to #{local}"
149
- scp.download!(remote,local)
150
- end
151
- else
152
- Net::SCP.start(@hostname, @vm_user, :password => @vm_pass, :auth_methods => ["password"]) do |scp|
153
- puts "DEBUG: downloading #{remote} to #{local}"
154
- scp.download!(remote,local)
155
- end
156
- end
157
- end
158
-
159
- def ssh_exec(command)
160
- if @vm_keyfile
161
- #puts "DEBUG: authenticating to #{@hostname} as #{@vm_user} with key #{@vm_keyfile}"
162
- Net::SSH.start(@hostname, @vm_user, :keys => [@vm_keyfile]) do |ssh|
163
- puts "DEBUG: running command: #{command}"
164
- ssh.exec!(command)
165
- end
166
- else
167
- Net::SSH.start(@hostname, @vm_user, :password => @vm_pass, :auth_methods => ["password"]) do |ssh|
168
- result = ssh.exec!(command)
169
- end
170
- end
171
- end
172
-
173
- def filter_input(string)
174
- return "" unless string # nil becomes empty string
175
- return unless string.class == String # Allow other types unmodified
176
-
177
- unless /^[\d\w\s\[\]\{\}\/\\\.\-\"\(\):!]*$/.match string
178
- raise "WARNING! Invalid character in: #{string}"
179
- end
180
- string
181
- end
182
-
183
- def filter_command(string)
184
- return "" unless string # nil becomes empty string
185
- return unless string.class == String # Allow other types unmodified
186
-
187
- unless /^[\d\w\s\[\]\{\}\/\\\.\-\"\(\)]*$/.match string
188
- raise "WARNING! Invalid character in: #{string}"
189
- end
190
- string
191
- end
192
-
193
- end
194
- end
195
- end
@@ -1,120 +0,0 @@
1
- require 'vm_driver'
2
-
3
- ##
4
- ## $Id$
5
- ##
6
-
7
- # This driver was built against:
8
- # VMware Vsphere 4.1
9
-
10
- module Lab
11
- module Drivers
12
-
13
- class VsphereDriver < VmDriver
14
-
15
- def initialize(config)
16
- unless config['user'] then raise ArgumentError, "Must provide a username" end
17
- unless config['host'] then raise ArgumentError, "Must provide a hostname" end
18
- unless config[''] then raise ArgumentError, "Must provide a password" end
19
- super(config)
20
-
21
- @user = filter_command(config['user'])
22
- @host = filter_command(config['host'])
23
-
24
- # Soft dependency
25
- begin
26
- require 'rbvmomi'
27
- rescue LoadError
28
- raise "WARNING: Library rbvmomi not found. Could not create driver!"
29
- end
30
-
31
- vim = RbVmomi::VIM.connect host: @host, user: @user, password: @pass
32
- dc = vim.serviceInstance.find_datacenter("datacenter1") or fail "datacenter not found"
33
- @vm = dc.find_vm("test") or fail "VM not found"
34
- end
35
-
36
- def start
37
- @vm.PowerOnVM_Task.wait_for_completion
38
- end
39
-
40
- def stop
41
- @vm.PowerOffVM_Task.wait_for_completion
42
- end
43
-
44
- def suspend
45
- @vm.SuspendVM_Task.wait_for_completion
46
- end
47
-
48
- def pause
49
- raise "Unimplemented"
50
- end
51
-
52
- def resume
53
- raise "Unimplemented"
54
- end
55
-
56
- def reset
57
- @vm.ResetVM_Task.wait_for_completion
58
- end
59
-
60
- def create_snapshot(snapshot)
61
- snapshot = filter_input(snapshot)
62
- raise "Unimplemented"
63
- end
64
-
65
- def revert_snapshot(snapshot)
66
- raise "Unimplemented"
67
- # If we got here, the snapshot didn't exist
68
- raise "Invalid Snapshot Name"
69
- end
70
-
71
- def delete_snapshot(snapshot, remove_children=false)
72
- raise "Unimplemented"
73
- # If we got here, the snapshot didn't exist
74
- raise "Invalid Snapshot Name"
75
- end
76
-
77
- def delete_all_snapshots
78
- raise "Unimplemented"
79
- end
80
-
81
- def run_command(command)
82
- raise "Unimplemented"
83
- end
84
-
85
- def copy_from(from, to)
86
- if @os == "linux"
87
- scp_from(from, to)
88
- else
89
- raise "Unimplemented"
90
- end
91
- end
92
-
93
- def copy_to(from, to)
94
- if @os == "linux"
95
- scp_to(from, to)
96
- else
97
- raise "Unimplemented"
98
- end
99
- end
100
-
101
- def check_file_exists(file)
102
- raise "Unimplemented"
103
- end
104
-
105
- def create_directory(directory)
106
- raise "Unimplemented"
107
- end
108
-
109
- def cleanup
110
- raise "Unimplemented"
111
- end
112
-
113
- def running?
114
- raise "Unimplemented"
115
- end
116
-
117
- end
118
-
119
- end
120
- end
@@ -1,234 +0,0 @@
1
- require 'vm_driver'
2
-
3
- ##
4
- ## $Id$
5
- ##
6
-
7
- module Lab
8
- module Drivers
9
-
10
- class WorkstationDriver < VmDriver
11
-
12
- def initialize(config)
13
- super(config)
14
-
15
- if !File.exist?(@location)
16
- raise ArgumentError,"Couldn't find: #{@location}"
17
- end
18
- end
19
-
20
- def start
21
- system_command("vmrun -T ws start " + "\'#{@location}\' nogui")
22
- end
23
-
24
- def stop
25
- system_command("vmrun -T ws stop " + "\'#{@location}\' nogui")
26
- end
27
-
28
- def suspend
29
- system_command("vmrun -T ws suspend " + "\'#{@location}\' nogui")
30
- end
31
-
32
- def pause
33
- system_command("vmrun -T ws pause " + "\'#{@location}\' nogui")
34
- end
35
-
36
- def reset
37
- system_command("vmrun -T ws reset " + "\'#{@location}\' nogui")
38
- end
39
-
40
- def create_snapshot(snapshot)
41
- snapshot = filter_input(snapshot)
42
- system_command("vmrun -T ws snapshot " + "\'#{@location}\' \'#{snapshot}\' nogui")
43
- end
44
-
45
- def revert_snapshot(snapshot)
46
- snapshot = filter_input(snapshot)
47
- system_command("vmrun -T ws revertToSnapshot " + "\'#{@location}\' \'#{snapshot}\' nogui")
48
- end
49
-
50
- def delete_snapshot(snapshot)
51
- snapshot = filter_input(snapshot)
52
- system_command("vmrun -T ws deleteSnapshot " + "\'#{@location}\' \'#{snapshot}\' nogui" )
53
- end
54
-
55
- def run_command(command)
56
-
57
- #
58
- # Generate a script name
59
- #
60
- script_rand_name = rand(1000000)
61
-
62
- #
63
- # Configure paths for each OS - We really can't filter command, so we're gonna
64
- # stick it in a script
65
- #
66
- if @os == "windows"
67
- local_tempfile_path = "/tmp/lab_script_#{script_rand_name}.bat"
68
- remote_tempfile_path = "C:\\\\lab_script_#{script_rand_name}.bat"
69
- remote_output_file = "C:\\\\lab_command_output_#{script_rand_name}"
70
- remote_run_command = remote_tempfile_path
71
- File.open(local_tempfile_path, 'w') {|f| f.write(command) }
72
- else
73
-
74
- local_tempfile_path = "/tmp/lab_script_#{script_rand_name}.sh"
75
- remote_tempfile_path = "/tmp/lab_script_#{script_rand_name}.sh"
76
- remote_output_file = "/tmp/lab_command_output_#{script_rand_name}"
77
- local_output_file = "/tmp/lab_command_output_#{script_rand_name}"
78
-
79
- remote_run_command = remote_tempfile_path
80
-
81
- File.open(local_tempfile_path, 'w') {|f| f.write("#!/bin/sh\n#{command}\n")}
82
- end
83
-
84
- if @tools
85
-
86
- #puts "DEBUG: Running w/ tools"
87
-
88
- #
89
- # Copy our local tempfile to the guest
90
- #
91
- vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " +
92
- "copyFileFromHostToGuest \'#{@location}\' \'#{local_tempfile_path}\'" +
93
- " \'#{remote_tempfile_path}\'"
94
- system_command(vmrunstr)
95
-
96
- if @os == "linux"
97
- #
98
- # Now run the command directly on the guest (linux - call w/ /bin/sh)
99
- #
100
- vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " +
101
- "runProgramInGuest \'#{@location}\' /bin/sh #{remote_tempfile_path} > #{remote_output_file}"
102
- system_command(vmrunstr)
103
- else
104
- #
105
- # Now run the command directly on the guest (windows)
106
- #
107
- vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " +
108
- "runProgramInGuest \'#{@location}\' #{remote_tempfile_path} > #{remote_output_file}"
109
- system_command(vmrunstr)
110
- end
111
-
112
- #
113
- # Cleanup. Delete it on the guest
114
- #
115
- vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} " +
116
- "deleteFileInGuest \'#{@location}\' \'#{remote_tempfile_path}\'"
117
- system_command(vmrunstr)
118
-
119
- #
120
- # Delete it locally
121
- #
122
- local_delete_command = "rm #{local_tempfile_path}"
123
- system_command(local_delete_command)
124
- else
125
-
126
- #
127
- # Use SCP / SSH
128
- #
129
-
130
- if @os == "linux"
131
-
132
- #
133
- # Copy it over
134
- #
135
- scp_to(local_tempfile_path, remote_tempfile_path)
136
-
137
- #
138
- # And ... execute it
139
- #
140
- ssh_exec("/bin/sh #{remote_tempfile_path} > #{remote_output_file}")
141
-
142
- #
143
- # Now copy the output back to us
144
- #
145
- scp_from(remote_output_file, local_output_file)
146
-
147
- # Now, let's look at the output of the command
148
- output_string = File.open(local_output_file,"r").read
149
-
150
- #
151
- # And clean up
152
- #
153
- ssh_exec("rm #{remote_output_file}")
154
- ssh_exec("rm #{remote_tempfile_path}")
155
-
156
- `rm #{local_output_file}`
157
-
158
- else
159
- raise "Hey, no tools, and windows? can't do nuttin for ya man."
160
- end
161
-
162
- end
163
- output_string
164
- end
165
-
166
- def copy_from(from, to)
167
- from = filter_input(from)
168
- to = filter_input(to)
169
- if @tools
170
- vmrunstr = "vmrun -T ws -gu \'#{@vm_user}\' -gp \'#{@vm_pass}\' copyFileFromGuestToHost " +
171
- "\'#{@location}\' \'#{from}\' \'#{to}\'"
172
- else
173
- scp_from(from, to)
174
- end
175
- system_command(vmrunstr)
176
- end
177
-
178
- def copy_to(from, to)
179
- from = filter_input(from)
180
- to = filter_input(to)
181
- if @tools
182
- vmrunstr = "vmrun -T ws -gu #{@vm_user} -gp #{@vm_pass} copyFileFromHostToGuest " +
183
- "\'#{@location}\' \'#{from}\' \'#{to}\'"
184
- system_command(vmrunstr)
185
- else
186
- scp_to(from, to)
187
- end
188
- end
189
-
190
- def check_file_exists(file)
191
- file = filter_input(file)
192
- if @tools
193
- vmrunstr = "vmrun -T ws -gu \'#{@vm_user}\' -gp \'#{@vm_pass}\' fileExistsInGuest " +
194
- "\'#{@location}\' \'#{file}\'"
195
- system_command(vmrunstr)
196
- else
197
- raise "Unsupported"
198
- end
199
- end
200
-
201
- def create_directory(directory)
202
- directory = filter_input(directory)
203
- if @tools
204
- vmrunstr = "vmrun -T ws -gu \'#{@vm_user}\' -gp \'#{@vm_pass}\' createDirectoryInGuest " +
205
- " \'#{@location}\' \'#{directory}\' "
206
- system_command(vmrunstr)
207
- else
208
- raise "Unsupported"
209
- end
210
- end
211
-
212
- def cleanup
213
-
214
- end
215
-
216
- def running?
217
- ## Get running Vms
218
- running = `vmrun list`
219
- running_array = running.split("\n")
220
- running_array.shift
221
-
222
- running_array.each do |vmx|
223
- if vmx.to_s == @location.to_s
224
- return true
225
- end
226
- end
227
-
228
- return false
229
- end
230
-
231
- end
232
-
233
- end
234
- end
@@ -1,126 +0,0 @@
1
- require 'vm_driver'
2
- ##
3
- ## $Id$
4
- ##
5
-
6
- # This requires rhythmx's vixr driver from https://github.com/rhythmx/vixr
7
- # and below that, the VIX api from vmware http://www.vmware.com/support/developer/vix-api/
8
-
9
- module Lab
10
- module Drivers
11
-
12
- class WorkstationVixrDriver < VmDriver
13
-
14
- attr_accessor :type
15
- attr_accessor :location
16
-
17
- def initialize(vmid, location, os=nil, tools=false, credentials=nil)
18
-
19
- # We have to treat this differently, as it's not in the same tree
20
- begin
21
- require 'vixr'
22
- rescue LoadError
23
- puts "WARNING: Library pro_vixr not found. To resolve this error, please\n" +
24
- " install the vixr gem. Latest is available here:\n" +
25
- "https://github.com/rhythmx/vixr ."
26
- raise "Unable to create vixr driver"
27
- end
28
-
29
- @vmid = filter_command(vmid)
30
- @location = filter_command(location)
31
-
32
- if !File.exist?(@location)
33
- raise ArgumentError,"Couldn't find: " + location
34
- end
35
-
36
- @credentials = credentials
37
- @tools = tools # not used in command lines, no filter
38
- @os = os # not used in command lines, no filter
39
-
40
- # TODO - Currently only implemented for the first set
41
- if @credentials.count > 0
42
- @vm_user = filter_input(@credentials[0]['user']) || "\'\'"
43
- @vm_pass = filter_input(@credentials[0]['pass']) || "\'\'"
44
- end
45
-
46
- host = VixR.connect()
47
- vm = host.open_vmx(@location)
48
-
49
- end
50
-
51
- def start
52
- vm.power_on
53
- end
54
-
55
- def stop
56
- vm.power_off
57
- end
58
-
59
- def suspend
60
- vm.suspend
61
- end
62
-
63
- def pause
64
- vm.pause
65
- end
66
-
67
- def reset
68
- vm.reset
69
- end
70
-
71
- def create_snapshot(snapshot)
72
- snapshot = filter_input(snapshot)
73
- system_command("ssh #{@user}@#{@host} vmrun -T ws snapshot \\\'#{@location}\\\' #{snapshot} nogui")
74
- end
75
-
76
- def revert_snapshot(snapshot)
77
- snapshot = filter_input(snapshot)
78
- system_command("ssh #{@user}@#{@host} vmrun -T ws revertToSnapshot \\\'#{@location}\\\' #{snapshot} nogui")
79
- end
80
-
81
- def delete_snapshot(snapshot)
82
- snapshot = filter_input(snapshot)
83
- system_command("ssh #{@user}@#{@host} vmrun -T ws deleteSnapshot \\\'#{@location}\\\' #{snapshot} nogui" )
84
- end
85
-
86
-
87
- def run_command(command)
88
- command = filter_input(command)
89
- if vm.login(@vm_user,@vm_pass)
90
- vm.run_prog(command)
91
- end
92
- end
93
-
94
- def copy_from(from, to)
95
- from = filter_input(from)
96
- to = filter_input(to)
97
- cp_from_host(from,to)
98
- end
99
-
100
- def copy_to(from, to)
101
- from = filter_input(from)
102
- to = filter_input(to)
103
- vm.cp_to_guest(from,to)
104
- end
105
-
106
- def check_file_exists(file)
107
- file = filter_input(file)
108
- file_exists?(file)
109
- end
110
-
111
- def create_directory(directory)
112
- directory = filter_input(directory)
113
- end
114
-
115
- def cleanup
116
-
117
- end
118
-
119
- def running?
120
- vm.running?
121
- end
122
-
123
- end
124
-
125
- end
126
- end
@@ -1,9 +0,0 @@
1
- require 'driver/workstation_driver'
2
- require 'driver/virtualbox_driver'
3
- require 'driver/fog_driver'
4
- require 'driver/dynagen_driver'
5
- require 'driver/remote_workstation_driver'
6
- require 'driver/remote_esxi_driver'
7
- require 'driver/vsphere_driver'
8
- #require 'driver/qemu_driver'
9
- #require 'driver/qemudo_driver'
@@ -1,16 +0,0 @@
1
- module Lab
2
- module Modifier
3
- module Backtrack5
4
-
5
- def nmap(options)
6
- run_command("nmap #{filter_input(options)}")
7
- end
8
-
9
- def testssl(site)
10
- run_command("/pentest/scanners/testssl/testssl.sh #{filter_input(site)}")
11
- end
12
-
13
- end
14
- end
15
- end
16
-
@@ -1,14 +0,0 @@
1
- # This assumes you're on a recent ubuntu
2
- # TODO - enforce this, or split it out...
3
-
4
- module Lab
5
- module Modifier
6
- module Dos
7
-
8
- def ping(target)
9
- run_command("ping #{filter_input(target)}")
10
- end
11
-
12
- end
13
- end
14
- end