fog-vsphere 1.7.0 → 1.7.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -1
  3. data/.rubocop.yml +8 -0
  4. data/.rubocop_todo.yml +217 -0
  5. data/.travis.yml +6 -7
  6. data/CHANGELOG.md +2 -6
  7. data/CONTRIBUTORS.md +10 -0
  8. data/Rakefile +10 -1
  9. data/lib/fog/vsphere/compute.rb +0 -2
  10. data/lib/fog/vsphere/models/compute/scsicontroller.rb +1 -6
  11. data/lib/fog/vsphere/models/compute/server.rb +7 -29
  12. data/lib/fog/vsphere/models/compute/ticket.rb +16 -0
  13. data/lib/fog/vsphere/models/compute/tickets.rb +25 -0
  14. data/lib/fog/vsphere/models/compute/volume.rb +28 -46
  15. data/lib/fog/vsphere/models/compute/volumes.rb +1 -1
  16. data/lib/fog/vsphere/requests/compute/create_vm.rb +44 -32
  17. data/lib/fog/vsphere/requests/compute/host_finish_maintenance.rb +14 -0
  18. data/lib/fog/vsphere/requests/compute/host_shutdown.rb +14 -0
  19. data/lib/fog/vsphere/requests/compute/host_start_maintenance.rb +14 -0
  20. data/lib/fog/vsphere/requests/compute/list_vm_scsi_controllers.rb +11 -4
  21. data/lib/fog/vsphere/requests/compute/list_vm_volumes.rb +1 -2
  22. data/lib/fog/vsphere/requests/compute/modify_vm_controller.rb +14 -2
  23. data/lib/fog/vsphere/requests/compute/modify_vm_volume.rb +3 -3
  24. data/lib/fog/vsphere/requests/compute/vm_acquire_ticket.rb +34 -0
  25. data/lib/fog/vsphere/requests/compute/vm_reconfig_volumes.rb +15 -26
  26. data/lib/fog/vsphere/requests/compute/vm_relocate.rb +54 -0
  27. data/lib/fog/vsphere/requests/compute/vm_remove_snapshot.rb +29 -0
  28. data/lib/fog/vsphere/requests/compute/vm_rename.rb +24 -0
  29. data/lib/fog/vsphere/requests/compute/vm_revert_snapshot.rb +29 -0
  30. data/lib/fog/vsphere/requests/compute/vm_suspend.rb +54 -0
  31. data/lib/fog/vsphere/version.rb +1 -1
  32. data/tests/class_from_string_tests.rb +3 -3
  33. data/tests/compute_tests.rb +16 -17
  34. data/tests/helpers/mock_helper.rb +3 -3
  35. data/tests/models/compute/cluster_tests.rb +4 -5
  36. data/tests/models/compute/hosts_tests.rb +2 -4
  37. data/tests/models/compute/rules_tests.rb +10 -16
  38. data/tests/models/compute/server_tests.rb +33 -29
  39. data/tests/models/compute/servers_tests.rb +2 -4
  40. data/tests/models/compute/ticket_tests.rb +12 -0
  41. data/tests/models/compute/tickets_tests.rb +8 -0
  42. data/tests/requests/compute/current_time_tests.rb +2 -4
  43. data/tests/requests/compute/folder_destroy_tests.rb +5 -7
  44. data/tests/requests/compute/get_network_tests.rb +18 -22
  45. data/tests/requests/compute/list_child_snapshots_tests.rb +1 -2
  46. data/tests/requests/compute/list_clusters_tests.rb +5 -6
  47. data/tests/requests/compute/list_datastores_tests.rb +6 -7
  48. data/tests/requests/compute/list_hosts_tests.rb +3 -4
  49. data/tests/requests/compute/list_networks_tests.rb +6 -7
  50. data/tests/requests/compute/list_storage_pods_test.rb +3 -4
  51. data/tests/requests/compute/list_virtual_machines_tests.rb +16 -20
  52. data/tests/requests/compute/list_vm_cdroms_tests.rb +1 -2
  53. data/tests/requests/compute/list_vm_snapshots_tests.rb +1 -2
  54. data/tests/requests/compute/modify_vm_cdrom_tests.rb +3 -4
  55. data/tests/requests/compute/revert_to_snapshot_tests.rb +2 -4
  56. data/tests/requests/compute/set_vm_customvalue_tests.rb +0 -2
  57. data/tests/requests/compute/vm_clone_tests.rb +20 -20
  58. data/tests/requests/compute/vm_config_vnc_tests.rb +3 -4
  59. data/tests/requests/compute/vm_destroy_tests.rb +1 -4
  60. data/tests/requests/compute/vm_migrate_tests.rb +1 -2
  61. data/tests/requests/compute/vm_power_off_tests.rb +2 -4
  62. data/tests/requests/compute/vm_power_on_tests.rb +1 -3
  63. data/tests/requests/compute/vm_reboot_tests.rb +2 -4
  64. data/tests/requests/compute/vm_reconfig_cdrom_tests.rb +2 -3
  65. data/tests/requests/compute/vm_reconfig_cpus_tests.rb +1 -3
  66. data/tests/requests/compute/vm_reconfig_hardware_tests.rb +2 -4
  67. data/tests/requests/compute/vm_reconfig_memory_tests.rb +1 -3
  68. data/tests/requests/compute/vm_suspend_tests.rb +23 -0
  69. data/tests/requests/compute/vm_take_snapshot_tests.rb +1 -3
  70. metadata +22 -4
  71. data/gemfiles/Gemfile.1.9.2+ +0 -9
@@ -0,0 +1,16 @@
1
+ require 'fog/compute/models/server'
2
+
3
+ module Fog
4
+ module Compute
5
+ class Vsphere
6
+ class Ticket < Fog::Model
7
+ attribute :server_id
8
+
9
+ attribute :ticket
10
+ attribute :host
11
+ attribute :port
12
+ attribute :ssl_thumbprint
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ module Fog
2
+ module Compute
3
+ class Vsphere
4
+ class Tickets < Fog::Collection
5
+ autoload :Ticket, File.expand_path('../ticket', __FILE__)
6
+
7
+ model Fog::Compute::Vsphere::Ticket
8
+
9
+ attr_accessor :server
10
+
11
+ def create(opts = {})
12
+ requires :server
13
+ raise 'server must be a Fog::Compute::Vsphere::Server' unless server.is_a?(Fog::Compute::Vsphere::Server)
14
+ new(
15
+ service.vm_acquire_ticket(
16
+ opts.merge(
17
+ 'instance_uuid' => server.instance_uuid
18
+ )
19
+ )
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -5,7 +5,6 @@ module Fog
5
5
  DISK_SIZE_TO_GB = 1048576
6
6
  identity :id
7
7
 
8
- has_one :server, Server
9
8
  attribute :datastore
10
9
  attribute :storage_pod
11
10
  attribute :mode
@@ -17,9 +16,12 @@ module Fog
17
16
  attribute :size_gb
18
17
  attribute :key
19
18
  attribute :unit_number
20
- attribute :controller_key, :type => :integer
19
+ attribute :server_id
20
+
21
+ def initialize(attributes={} )
22
+ # Assign server first to prevent race condition with persisted?
23
+ self.server_id = attributes.delete(:server_id)
21
24
 
22
- def initialize(attributes={})
23
25
  super defaults.merge(attributes)
24
26
  end
25
27
 
@@ -29,7 +31,6 @@ module Fog
29
31
 
30
32
  def size_gb= s
31
33
  attributes[:size] = s.to_i * DISK_SIZE_TO_GB if s
32
- attributes[:size_gb] = s.to_i if s
33
34
  end
34
35
 
35
36
  def to_s
@@ -47,7 +48,25 @@ module Fog
47
48
  raise Fog::Errors::Error.new('Resaving an existing object may create a duplicate') if persisted?
48
49
  requires :server_id, :size, :datastore
49
50
 
50
- set_unit_number
51
+ # When adding volumes to vsphere, if our unit_number is 7 or higher, vsphere will increment the unit_number
52
+ # This is due to SCSI ID 7 being reserved for the pvscsi controller
53
+ # When referring to a volume that already added using a unit_id of 7 or higher, we must refer to the actual SCSI ID
54
+ if unit_number.nil?
55
+ # Vsphere maps unit_numbers 7 and greater to a higher SCSI ID since the pvscsi driver reserves SCSI ID 7
56
+ used_unit_numbers = server.volumes.map { |volume| volume.unit_number < 7 ? volume.unit_number : volume.unit_number - 1 }
57
+ max_unit_number = used_unit_numbers.max
58
+
59
+ if max_unit_number > server.volumes.size
60
+ # If the max ID exceeds the number of volumes, there must be a hole in the range. Find a hole and use it.
61
+ self.unit_number = max_unit_number.times.to_a.find { |i| used_unit_numbers.exclude?(i) }
62
+ else
63
+ self.unit_number = max_unit_number + 1
64
+ end
65
+ else
66
+ if server.volumes.any? { |volume| volume.unit_number == self.unit_number && volume.id != self.id }
67
+ raise "A volume already exists with that unit_number, so we can't save the new volume"
68
+ end
69
+ end
51
70
 
52
71
  data = service.add_vm_volume(self)
53
72
 
@@ -61,7 +80,6 @@ module Fog
61
80
 
62
81
  self.id = created.id
63
82
  self.key = created.key
64
- self.controller_key = created.controllerKey
65
83
  self.filename = created.filename
66
84
 
67
85
  true
@@ -70,33 +88,9 @@ module Fog
70
88
  end
71
89
  end
72
90
 
73
- def server_id
74
- requires :server
75
- server.id
76
- end
77
-
78
- def set_unit_number
79
- # When adding volumes to vsphere, if our unit_number is 7 or higher, vsphere will increment the unit_number
80
- # This is due to SCSI ID 7 being reserved for the pvscsi controller
81
- # When referring to a volume that already added using a unit_id of 7 or higher, we must refer to the actual SCSI ID
82
- if unit_number.nil?
83
- self.unit_number = calculate_free_unit_number
84
- else
85
- if server.volumes.select { |vol| vol.controller_key == controller_key }.any? { |volume| volume.unit_number == self.unit_number && volume.id != self.id }
86
- raise "A volume already exists with that unit_number, so we can't save the new volume"
87
- end
88
- end
89
- end
90
-
91
- def set_key
92
- requires :controller_key, :unit_number
93
-
94
- return unless key.nil?
95
-
96
- # controller key is based on 1000 + controller bus
97
- # disk key is based on 2000 + the SCSI ID + the controller bus * 16
98
- controller_bus = controller_key - 1000
99
- self.key = 2000 + (controller_bus * 16) + unit_number
91
+ def server
92
+ requires :server_id
93
+ service.servers.get(server_id)
100
94
  end
101
95
 
102
96
  private
@@ -105,21 +99,9 @@ module Fog
105
99
  {
106
100
  :thin => true,
107
101
  :name => "Hard disk",
108
- :mode => "persistent",
109
- :controller_key => 1000
102
+ :mode => "persistent"
110
103
  }
111
104
  end
112
-
113
- def calculate_free_unit_number
114
- requires :controller_key
115
-
116
- # Vsphere maps unit_numbers 7 and greater to a higher SCSI ID since the pvscsi driver reserves SCSI ID 7
117
- used_unit_numbers = server.volumes
118
- .select { |vol| vol.unit_number && vol.controller_key == controller_key }.map(&:unit_number) + [7]
119
- free_unit_numbers = (0..15).to_a - used_unit_numbers
120
-
121
- free_unit_numbers.first
122
- end
123
105
  end
124
106
  end
125
107
  end
@@ -20,7 +20,7 @@ module Fog
20
20
  raise 'volumes should have vm or template'
21
21
  end
22
22
 
23
- self.each { |volume| volume.server = server }
23
+ self.each { |volume| volume.server_id = server.id }
24
24
  self
25
25
  end
26
26
 
@@ -20,7 +20,7 @@ module Fog
20
20
  vm_cfg[:cpuHotAddEnabled] = attributes[:cpuHotAddEnabled] if attributes.key?(:cpuHotAddEnabled)
21
21
  vm_cfg[:memoryHotAddEnabled] = attributes[:memoryHotAddEnabled] if attributes.key?(:memoryHotAddEnabled)
22
22
  vm_cfg[:firmware] = attributes[:firmware] if attributes.key?(:firmware)
23
- vm_cfg[:bootOptions] = boot_options(attributes, vm_cfg) if attributes.key?(:boot_order) || attributes.key?(:boot_retry)
23
+ vm_cfg[:bootOptions] = boot_options(attributes) if attributes.key?(:boot_order) || attributes.key?(:boot_retry)
24
24
  resource_pool = if attributes[:resource_pool] && attributes[:resource_pool] != 'Resources'
25
25
  get_raw_resource_pool(attributes[:resource_pool], attributes[:cluster], attributes[:datacenter])
26
26
  else
@@ -97,12 +97,9 @@ module Fog
97
97
  devices << nics.map { |nic| create_interface(nic, nics.index(nic), :add, attributes) }
98
98
  end
99
99
 
100
- if (scsi_controllers = (attributes[:scsi_controllers] || attributes["scsi_controller"]))
101
- devices << scsi_controllers.each_with_index.map { |controller, index| create_controller(controller, index) }
102
- end
103
-
104
100
  if (disks = attributes[:volumes])
105
- devices << disks.map { |disk| create_disk(disk, :add, get_storage_pod(attributes)) }
101
+ devices << create_controller(attributes[:scsi_controller]||attributes["scsi_controller"]||{})
102
+ devices << disks.map { |disk| create_disk(disk, disks.index(disk), :add, 1000, get_storage_pod(attributes)) }
106
103
  end
107
104
 
108
105
  if (cdroms = attributes[:cdroms])
@@ -111,12 +108,12 @@ module Fog
111
108
  devices.flatten
112
109
  end
113
110
 
114
- def boot_options(attributes, vm_cfg)
111
+ def boot_options attributes
115
112
  # NOTE: you must be using vsphere_rev 5.0 or greater to set boot_order
116
113
  # e.g. Fog::Compute.new(provider: "vsphere", vsphere_rev: "5.5", etc)
117
114
  options = {}
118
115
  if @vsphere_rev.to_f >= 5 and attributes[:boot_order]
119
- options[:bootOrder] = boot_order(attributes, vm_cfg)
116
+ options[:bootOrder] = boot_order(attributes)
120
117
  end
121
118
 
122
119
  # Set attributes[:boot_retry] to a delay in miliseconds to enable boot retries
@@ -124,11 +121,11 @@ module Fog
124
121
  options[:bootRetryEnabled] = true
125
122
  options[:bootRetryDelay] = attributes[:boot_retry]
126
123
  end
127
-
124
+
128
125
  options.empty? ? nil : RbVmomi::VIM::VirtualMachineBootOptions.new(options)
129
126
  end
130
127
 
131
- def boot_order(attributes, vm_cfg)
128
+ def boot_order attributes
132
129
  # attributes[:boot_order] may be an array like this ['network', 'disk']
133
130
  # stating, that we want to prefer network boots over disk boots
134
131
  boot_order = []
@@ -145,17 +142,18 @@ module Fog
145
142
  end
146
143
  end
147
144
  when 'disk'
148
- disks = vm_cfg[:deviceChange].map {|dev| dev[:device]}.select { |dev| dev.is_a? RbVmomi::VIM::VirtualDisk }
149
- disks.each do |disk|
150
- # we allow booting from all harddisks, the first disk has the highest priority
151
- boot_order << RbVmomi::VIM::VirtualMachineBootOptionsBootableDiskDevice.new(
152
- :deviceKey => disk.key
153
- )
145
+ if disks = attributes[:volumes]
146
+ disks.each do |disk|
147
+ # we allow booting from all harddisks, the first disk has the highest priority
148
+ boot_order << RbVmomi::VIM::VirtualMachineBootOptionsBootableDiskDevice.new(
149
+ :deviceKey => disk.key || get_disk_device_key(disks.index(disk)),
150
+ )
151
+ end
154
152
  end
155
153
  when 'cdrom'
156
- boot_order << RbVmomi::VIM::VirtualMachineBootOptionsBootableCdromDevice.new
154
+ boot_order << RbVmomi::VIM::VirtualMachineBootOptionsBootableCdromDevice.new()
157
155
  when 'floppy'
158
- boot_order << RbVmomi::VIM::VirtualMachineBootOptionsBootableFloppyDevice.new
156
+ boot_order << RbVmomi::VIM::VirtualMachineBootOptionsBootableFloppyDevice.new()
159
157
  else
160
158
  raise "failed to create boot device because \"#{boot_device}\" is unknown"
161
159
  end
@@ -163,6 +161,19 @@ module Fog
163
161
  boot_order
164
162
  end
165
163
 
164
+ def get_disk_device_key(index)
165
+ # disk key is based on 2000 + the SCSI ID + the controller bus * 16
166
+ # the scsi host adapter appears as SCSI ID 7, so we have to skip that
167
+ # host adapter key is based on 1000 + bus id
168
+ # fog assumes that there is only a single scsi controller, see device_change()
169
+ if (index > 6) then
170
+ _index = index + 1
171
+ else
172
+ _index = index
173
+ end
174
+ 2000 + _index
175
+ end
176
+
166
177
  def create_nic_backing nic, attributes
167
178
  raw_network = get_raw_network(nic.network, attributes[:datacenter], if nic.virtualswitch then nic.virtualswitch end)
168
179
 
@@ -193,9 +204,9 @@ module Fog
193
204
  }
194
205
  end
195
206
 
196
- def create_controller(controller=nil, index = 0)
197
- options=if controller
198
- controller_default_options.merge(controller.attributes)
207
+ def create_controller options=nil
208
+ options=if options
209
+ controller_default_options.merge(Hash[options.map{|k,v| [k.to_sym,v] }])
199
210
  else
200
211
  controller_default_options
201
212
  end
@@ -207,15 +218,15 @@ module Fog
207
218
  {
208
219
  :operation => options[:operation],
209
220
  :device => controller_class.new({
210
- :key => options[:key] || (1000 + index),
211
- :busNumber => options[:bus_id] || index,
221
+ :key => options[:key],
222
+ :busNumber => options[:bus_id],
212
223
  :sharedBus => controller_get_shared_from_options(options),
213
224
  })
214
225
  }
215
226
  end
216
227
 
217
228
  def controller_default_options
218
- {:operation => :add, :type => RbVmomi::VIM.VirtualLsiLogicController.class, :shared => false }
229
+ {:operation => "add", :type => RbVmomi::VIM.VirtualLsiLogicController.class, :key => 1000, :bus_id => 0, :shared => false }
219
230
  end
220
231
 
221
232
  def controller_get_shared_from_options options
@@ -230,29 +241,30 @@ module Fog
230
241
  end
231
242
  end
232
243
 
233
- def create_disk(disk, operation = :add, storage_pod = nil)
244
+ def create_disk disk, index = 0, operation = :add, controller_key = 1000, storage_pod = nil
245
+ if (index > 6) then
246
+ _index = index + 1
247
+ else
248
+ _index = index
249
+ end
234
250
  # If we deploy the vm on a storage pod, datastore has to be an empty string
235
251
  if storage_pod
236
252
  datastore = ''
237
253
  else
238
254
  datastore = "[#{disk.datastore}]"
239
255
  end
240
-
241
- disk.set_unit_number
242
- disk.set_key
243
-
244
256
  payload = {
245
257
  :operation => operation,
246
258
  :fileOperation => operation == :add ? :create : :destroy,
247
259
  :device => RbVmomi::VIM.VirtualDisk(
248
- :key => disk.key,
260
+ :key => disk.key || _index,
249
261
  :backing => RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo(
250
262
  :fileName => datastore,
251
263
  :diskMode => disk.mode.to_sym,
252
264
  :thinProvisioned => disk.thin
253
265
  ),
254
- :controllerKey => disk.controller_key,
255
- :unitNumber => disk.unit_number,
266
+ :controllerKey => controller_key,
267
+ :unitNumber => _index,
256
268
  :capacityInKB => disk.size
257
269
  )
258
270
  }
@@ -0,0 +1,14 @@
1
+ module Fog
2
+ module Compute
3
+ class Vsphere
4
+ class Real
5
+ def host_finish_maintenance(name, cluster_name, datacenter_name, timeout = 0)
6
+ host_ref = get_host(name, cluster_name, datacenter_name)
7
+ task = host_ref.ExitMaintenanceMode_Task(timeout: timeout)
8
+ task.wait_for_completion
9
+ { 'task_state' => task.info.state }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Fog
2
+ module Compute
3
+ class Vsphere
4
+ class Real
5
+ def host_shutdown(name, cluster_name, datacenter_name, force = false)
6
+ host_ref = get_host(name, cluster_name, datacenter_name)
7
+ task = host_ref.ShutdownHost_Task(force: force)
8
+ task.wait_for_completion
9
+ { 'task_state' => task.info.state }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Fog
2
+ module Compute
3
+ class Vsphere
4
+ class Real
5
+ def host_start_maintenance(name, cluster_name, datacenter_name, timeout = 0, evacuate_powered_off_vms = false)
6
+ host_ref = get_host(name, cluster_name, datacenter_name)
7
+ task = host_ref.EnterMaintenanceMode_Task(timeout: timeout, evacuatePoweredOffVms: evacuate_powered_off_vms)
8
+ task.wait_for_completion
9
+ { 'task_state' => task.info.state }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -11,14 +11,21 @@ module Fog
11
11
  def list_vm_scsi_controllers_raw(vm_id)
12
12
  get_vm_ref(vm_id).config.hardware.device.grep(RbVmomi::VIM::VirtualSCSIController).map do |ctrl|
13
13
  {
14
- :type => ctrl.class.to_s,
15
- :shared_bus => ctrl.sharedBus.to_s,
16
- :unit_number => ctrl.scsiCtlrUnitNumber,
17
- :key => ctrl.key,
14
+ type: ctrl.class.to_s,
15
+ shared_bus: ctrl.sharedBus.to_s,
16
+ unit_number: ctrl.scsiCtlrUnitNumber,
17
+ key: ctrl.key
18
18
  }
19
19
  end
20
20
  end
21
21
  end
22
+ class Mock
23
+ def list_vm_scsi_controllers(vm_id)
24
+ raise Fog::Compute::Vsphere::NotFound, 'VM not Found' unless data[:servers].key?(vm_id)
25
+ return [] unless data[:servers][vm_id].key?('scsi_controllers')
26
+ data[:servers][vm_id]['scsi_controllers'].map { |h| h.merge(server_id: vm_id) }
27
+ end
28
+ end
22
29
  end
23
30
  end
24
31
  end
@@ -38,8 +38,7 @@ module Fog
38
38
  :size => vol.capacityInKB,
39
39
  :name => vol.deviceInfo.label,
40
40
  :key => vol.key,
41
- :unit_number => vol.unitNumber,
42
- :controller_key => vol.controllerKey
41
+ :unit_number => vol.unitNumber
43
42
  }
44
43
  end
45
44
  end
@@ -3,13 +3,25 @@ module Fog
3
3
  class Vsphere
4
4
  class Real
5
5
  def add_vm_controller(controller)
6
- vm_reconfig_hardware('instance_uuid' => controller.server_id, 'hardware_spec' => {'deviceChange'=>[create_controller(controller)]})
6
+ options = {}
7
+ options[:operation] = 'add'
8
+ options[:type] = controller.type
9
+ options[:key] = controller.key
10
+ options[:shared] = controller.shared_bus
11
+
12
+ vm_reconfig_hardware('instance_uuid' => controller.server_id, 'hardware_spec' => {'deviceChange'=>[create_controller(options)]})
7
13
  end
8
14
  end
9
15
 
10
16
  class Mock
11
17
  def add_vm_controller(controller)
12
- vm_reconfig_hardware('instance_uuid' => controller.server_id, 'hardware_spec' => {'deviceChange'=>[create_controller(controller)]})
18
+ options = {}
19
+ options[:operation] = 'add'
20
+ options[:type] = RbVmomi::VIM.VirtualLsiLogicController.class
21
+ options[:key] = 1000
22
+ options[:shared] = false
23
+
24
+ vm_reconfig_hardware('instance_uuid' => controller.server_id, 'hardware_spec' => {'deviceChange'=>[create_controller(options)]})
13
25
  end
14
26
  end
15
27
  end