corl 0.4.15 → 0.4.16

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +6 -1
  3. data/Gemfile.lock +76 -30
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/bootstrap/bootstrap.sh +5 -2
  7. data/bootstrap/os/ubuntu/00_base.sh +1 -1
  8. data/bootstrap/os/ubuntu/01_git.sh +9 -0
  9. data/bootstrap/os/ubuntu/05_ruby.sh +7 -4
  10. data/bootstrap/os/ubuntu/06_puppet.sh +2 -2
  11. data/corl.gemspec +23 -9
  12. data/lib/CORL/action/authorize.rb +2 -5
  13. data/lib/CORL/action/bootstrap.rb +1 -5
  14. data/lib/CORL/action/build.rb +2 -10
  15. data/lib/CORL/action/destroy.rb +2 -7
  16. data/lib/CORL/action/exec.rb +1 -5
  17. data/lib/CORL/action/image.rb +1 -5
  18. data/lib/CORL/action/images.rb +12 -10
  19. data/lib/CORL/action/ip.rb +21 -0
  20. data/lib/CORL/action/lookup.rb +5 -3
  21. data/lib/CORL/action/machines.rb +12 -10
  22. data/lib/CORL/action/provision.rb +4 -7
  23. data/lib/CORL/action/regions.rb +12 -10
  24. data/lib/CORL/action/seed.rb +9 -10
  25. data/lib/CORL/action/spawn.rb +29 -15
  26. data/lib/CORL/action/ssh.rb +1 -5
  27. data/lib/CORL/action/start.rb +1 -5
  28. data/lib/CORL/action/stop.rb +1 -5
  29. data/lib/CORL/action/vagrantfile.rb +55 -0
  30. data/lib/CORL/configuration/file.rb +4 -1
  31. data/lib/CORL/machine/physical.rb +1 -1
  32. data/lib/CORL/machine/vagrant.rb +358 -0
  33. data/lib/CORL/node/local.rb +2 -3
  34. data/lib/CORL/node/vagrant.rb +238 -0
  35. data/lib/core/facade.rb +25 -2
  36. data/lib/core/mixin/macro/network_settings.rb +35 -1
  37. data/lib/core/plugin/action.rb +53 -5
  38. data/lib/core/plugin/configuration.rb +19 -5
  39. data/lib/core/plugin/fog_machine.rb +1 -1
  40. data/lib/core/plugin/fog_node.rb +9 -9
  41. data/lib/core/plugin/machine.rb +6 -13
  42. data/lib/core/plugin/network.rb +23 -7
  43. data/lib/core/plugin/node.rb +69 -36
  44. data/lib/core/plugin/provisioner.rb +1 -2
  45. data/lib/core/vagrant/Vagrantfile +7 -0
  46. data/lib/core/vagrant/commands/launcher.rb +66 -0
  47. data/lib/core/vagrant/config.rb +308 -0
  48. data/lib/core/vagrant/plugins.rb +33 -0
  49. data/lib/core/vagrant/provisioner/config.rb +39 -0
  50. data/lib/core/vagrant/provisioner/provisioner.rb +46 -0
  51. data/lib/corl.rb +8 -0
  52. data/locales/en.yml +13 -1
  53. metadata +120 -59
@@ -1,5 +1,29 @@
1
1
 
2
2
  module CORL
3
+ module Vagrant
4
+
5
+ #
6
+ # Since we can execute CORL actions from within Vagrant on a combination of
7
+ # Vagrant VMs and remote server instances we need a way to tap into the
8
+ # Vagrant environment and operate on CORL configured Vagrant machines.
9
+ #
10
+ # This command is set in the CORL launcher Vagrant command plugin execute
11
+ # method. It is then accessible anywhere within CORL if we have used that
12
+ # Vagrant command as an execution gateway. If not it will be nil, giving us
13
+ # a convienient method for checking whether we are executing through Vagrant
14
+ # which is used in the CORL Vagrant {Node} and {Machine} plugins.
15
+ #
16
+ @@command = nil
17
+
18
+ def self.command=command
19
+ @@command = command
20
+ end
21
+
22
+ def self.command
23
+ @@command
24
+ end
25
+ end
26
+
3
27
  module Plugin
4
28
  class CloudAction < CORL.plugin_class(:action)
5
29
 
@@ -38,10 +62,11 @@ class CloudAction < CORL.plugin_class(:action)
38
62
  true
39
63
  end
40
64
  register :node_provider, :str, :local, 'corl.core.action.options.node_provider' do |value|
41
- value = value.to_sym
65
+ value = value.to_sym
66
+ node_providers = node_plugins.keys
42
67
 
43
- unless node_plugins.keys.include?(value)
44
- warn('corl.core.action.errors.node_provider', { :value => value, :choices => node_plugins.keys.join(", ") })
68
+ unless CORL.vagrant? || node_providers.include?(value)
69
+ warn('corl.core.action.errors.node_provider', { :value => value, :choices => node_providers.join(", ") })
45
70
  next false
46
71
  end
47
72
  true
@@ -94,7 +119,7 @@ class CloudAction < CORL.plugin_class(:action)
94
119
  #
95
120
  # A fork in the road...
96
121
  #
97
- if network.has_nodes? && ! settings[:nodes].empty?
122
+ if network && network.has_nodes? && ! settings[:nodes].empty?
98
123
  # Execute action on remote nodes
99
124
  success = network.batch(settings[:nodes], settings[:node_provider], settings[:parallel]) do |node|
100
125
  exec_config = Config.new(settings)
@@ -110,7 +135,8 @@ class CloudAction < CORL.plugin_class(:action)
110
135
  myself.status = code.batch_error unless success
111
136
  else
112
137
  # Execute statement locally
113
- node = network.local_node
138
+ node = nil
139
+ node = network.local_node if network
114
140
 
115
141
  if validate(node, network)
116
142
  yield(node, network) if block_given?
@@ -149,6 +175,28 @@ class CloudAction < CORL.plugin_class(:action)
149
175
  # Implement in sub classes if needed
150
176
  data
151
177
  end
178
+
179
+ #---
180
+
181
+ def ensure_network(network, &block)
182
+ codes :network_failure
183
+
184
+ if network
185
+ block.call
186
+ else
187
+ myself.status = code.network_failure
188
+ end
189
+ end
190
+
191
+ def ensure_node(node, &block)
192
+ codes :node_failure
193
+
194
+ if node
195
+ block.call
196
+ else
197
+ myself.status = code.node_failure
198
+ end
199
+ end
152
200
  end
153
201
  end
154
202
  end
@@ -16,11 +16,13 @@ class Configuration < CORL.plugin_class(:base)
16
16
 
17
17
  logger.info("Setting source configuration project")
18
18
  @project = CORL.project(extended_config(:project, {
19
- :directory => _delete(:directory, Dir.pwd),
20
- :url => _delete(:url),
21
- :revision => _delete(:revision),
22
- :create => _delete(:create, false),
23
- :pull => true
19
+ :directory => _delete(:directory, Dir.pwd),
20
+ :url => _delete(:url),
21
+ :revision => _delete(:revision),
22
+ :create => _delete(:create, false),
23
+ :pull => true,
24
+ :internal_ip => CORL.public_ip, # Needed for seeding Vagrant VMs
25
+ :manage_ignore => true
24
26
  }), _delete(:project_provider))
25
27
 
26
28
  _init(:autoload, true)
@@ -53,6 +55,18 @@ class Configuration < CORL.plugin_class(:base)
53
55
 
54
56
  #---
55
57
 
58
+ def cache
59
+ project.cache
60
+ end
61
+
62
+ #---
63
+
64
+ def ignore(files)
65
+ project.ignore(files)
66
+ end
67
+
68
+ #---
69
+
56
70
  def autoload(default = false)
57
71
  _get(:autoload, default)
58
72
  end
@@ -149,7 +149,7 @@ class Fog < CORL.plugin_class(:machine)
149
149
 
150
150
  def download(remote_path, local_path, options = {})
151
151
  super do |config, success|
152
- logger.debug("Executing SCP download to #{local_path} from #{remote_path} on machine #{name}")
152
+ logger.debug("Executing SCP download to #{local_path} from #{remote_path} on machine #{plugin_name}")
153
153
 
154
154
  begin
155
155
  if init_ssh_session(server)
@@ -11,15 +11,15 @@ class Fog < CORL.plugin_class(:node)
11
11
  # Node plugin interface
12
12
 
13
13
  def normalize(reload)
14
- super do
15
- myself.region = region
14
+ super
15
+
16
+ myself.region = region
16
17
 
17
- unless reload
18
- machine_provider = :fog
19
- machine_provider = yield if block_given?
18
+ unless reload
19
+ machine_provider = :fog
20
+ machine_provider = yield if block_given?
20
21
 
21
- myself.machine = create_machine(:machine, machine_provider, machine_config)
22
- end
22
+ myself.machine = create_machine(:machine, machine_provider, machine_config)
23
23
  end
24
24
  end
25
25
 
@@ -81,8 +81,8 @@ class Fog < CORL.plugin_class(:node)
81
81
  if region = myself[:region]
82
82
  region
83
83
  else
84
- first_region = regions.first
85
- myself.region = first_region
84
+ first_region = regions.first
85
+ myself.region = first_region
86
86
  first_region
87
87
  end
88
88
  end
@@ -7,7 +7,6 @@ class Machine < CORL.plugin_class(:base)
7
7
  # Machine plugin interface
8
8
 
9
9
  def normalize(reload)
10
- myself.plugin_name = node[:id]
11
10
  end
12
11
 
13
12
  #-----------------------------------------------------------------------------
@@ -85,12 +84,6 @@ class Machine < CORL.plugin_class(:base)
85
84
  #-----------------------------------------------------------------------------
86
85
  # Management
87
86
 
88
- def init_ssh(ssh_port)
89
- # Implement in sub classes if needed
90
- end
91
-
92
- #---
93
-
94
87
  def load
95
88
  success = true
96
89
 
@@ -158,7 +151,7 @@ class Machine < CORL.plugin_class(:base)
158
151
  results = []
159
152
 
160
153
  if running?
161
- logger.debug("Executing command on #{plugin_provider} machine with: #{options.inspect}")
154
+ logger.debug("Executing commands on #{plugin_provider} machine with: #{options.inspect}")
162
155
  config = Config.ensure(options)
163
156
  results = yield(config, results) if block_given?
164
157
  else
@@ -246,12 +239,12 @@ class Machine < CORL.plugin_class(:base)
246
239
  else
247
240
  logger.debug("Starting #{plugin_provider} machine with: #{options.inspect}")
248
241
 
249
- if created?
250
- logger.debug("Machine #{plugin_name} has already been created")
242
+ logger.debug("Machine #{plugin_name} is not running yet")
243
+ if block_given?
244
+ success = yield(config)
251
245
  else
252
- logger.debug("Machine #{plugin_name} does not yet exist")
253
- success = create(options)
254
- end
246
+ success = create(options)
247
+ end
255
248
  end
256
249
 
257
250
  logger.warn("There was an error starting the machine #{plugin_name}") unless success
@@ -4,7 +4,6 @@ module Plugin
4
4
  class Network < CORL.plugin_class(:base)
5
5
 
6
6
  init_plugin_collection
7
- task_class TaskThread
8
7
 
9
8
  #-----------------------------------------------------------------------------
10
9
  # Cloud plugin interface
@@ -14,6 +13,8 @@ class Network < CORL.plugin_class(:base)
14
13
 
15
14
  logger.info("Initializing sub configuration from source with: #{myself._export.inspect}")
16
15
  myself.config = CORL.configuration(Config.new(myself._export).import({ :autosave => false, :create => false })) unless reload
16
+
17
+ ignore('build')
17
18
  end
18
19
 
19
20
  #-----------------------------------------------------------------------------
@@ -49,6 +50,18 @@ class Network < CORL.plugin_class(:base)
49
50
 
50
51
  #---
51
52
 
53
+ def cache
54
+ config.cache
55
+ end
56
+
57
+ #---
58
+
59
+ def ignore(files)
60
+ config.ignore(files)
61
+ end
62
+
63
+ #---
64
+
52
65
  def remote(name)
53
66
  config.remote(name)
54
67
  end
@@ -130,7 +143,7 @@ class Network < CORL.plugin_class(:base)
130
143
  #---
131
144
 
132
145
  def local_node(require_new = false)
133
- ip_address = CORL.ip_address
146
+ ip_address = CORL.public_ip
134
147
  local_node = node_by_ip(ip_address, require_new)
135
148
 
136
149
  if local_node.nil?
@@ -223,15 +236,18 @@ class Network < CORL.plugin_class(:base)
223
236
 
224
237
  remote_name = config.delete(:remote, :edit)
225
238
 
226
- # Set node data
227
- node = set_node(provider, name, {
228
- :settings => array(config.delete(:groups, [])),
239
+ node_options = Util::Data.clean({
240
+ :settings => array(config.delete(:groups, [])) | [ "server" ],
229
241
  :region => config.delete(:region, nil),
230
242
  :machine_type => config.delete(:machine_type, nil),
243
+ :public_ip => config.delete(:public_ip, nil),
231
244
  :image => config.delete(:image, nil),
232
245
  :user => config.delete(:user, :root),
233
246
  :hostname => name
234
247
  })
248
+
249
+ # Set node data
250
+ node = set_node(provider, name, node_options)
235
251
  hook_config = { :node => node, :remote => remote_name, :config => config }
236
252
  success = true
237
253
 
@@ -252,11 +268,11 @@ class Network < CORL.plugin_class(:base)
252
268
  if success
253
269
  seed_project = config.get(:project_reference, nil)
254
270
  save_config = { :commit => true, :remote => remote_name, :push => true }
255
-
271
+
256
272
  if seed_project && remote_name
257
273
  # Reset project remote
258
274
  seed_info = Plugin::Project.translate_reference(seed_project)
259
-
275
+
260
276
  if seed_info
261
277
  seed_url = seed_info[:url]
262
278
  seed_branch = seed_info[:revision] if seed_info[:revision]
@@ -4,8 +4,7 @@ module Plugin
4
4
  class Node < CORL.plugin_class(:base)
5
5
 
6
6
  include Celluloid
7
- task_class TaskThread
8
-
7
+
9
8
  #-----------------------------------------------------------------------------
10
9
  # Node plugin interface
11
10
 
@@ -18,12 +17,10 @@ class Node < CORL.plugin_class(:base)
18
17
  myself[name] = value
19
18
  end
20
19
 
21
- yield if block_given? # Chance to create a machine to feed hostname
22
-
23
20
  ui.resource = Util::Console.colorize(hostname, @class_color)
24
21
  logger = hostname
25
22
 
26
- myself[:settings] = [ "all", plugin_provider.to_s, plugin_name.to_s ] | setting(:settings, [], :array)
23
+ add_groups([ "all", plugin_provider.to_s, plugin_name.to_s ])
27
24
 
28
25
  unless reload
29
26
  @cli_interface = Util::Liquid.new do |method, args, &code|
@@ -317,7 +314,7 @@ class Node < CORL.plugin_class(:base)
317
314
 
318
315
  def machine_config
319
316
  name = myself[:id]
320
- name = myself[:hostname] if name.nil?
317
+ name = myself[:hostname] if name.nil? || name.empty?
321
318
  config = Config.new({ :name => name })
322
319
 
323
320
  yield(config) if block_given?
@@ -436,6 +433,8 @@ class Node < CORL.plugin_class(:base)
436
433
  if machine
437
434
  config = Config.ensure(options)
438
435
 
436
+ clear_cache
437
+
439
438
  if extension_check(:create, { :config => config })
440
439
  logger.info("Creating node: #{plugin_name}")
441
440
 
@@ -571,7 +570,8 @@ class Node < CORL.plugin_class(:base)
571
570
  execute_block_on_receiver :exec
572
571
 
573
572
  def exec(options = {})
574
- results = nil
573
+ default_error = Util::Shell::Result.new(:error, 255)
574
+ results = [ default_error ]
575
575
 
576
576
  if machine && machine.running?
577
577
  config = Config.ensure(options)
@@ -594,25 +594,35 @@ class Node < CORL.plugin_class(:base)
594
594
  results = active_machine.exec(commands, config.export) do |type, command, data|
595
595
  unless local?
596
596
  if type == :error
597
- alert(data)
597
+ alert(filter_output(type, data))
598
598
  else
599
- render(data)
599
+ render(filter_output(type, data))
600
600
  end
601
601
  end
602
602
  yield(:progress, { :type => type, :command => command, :data => data }) if block_given?
603
603
  end
604
+ else
605
+ default_error.append_errors("No execution command")
604
606
  end
605
607
 
606
- success = true
607
- results.each do |result|
608
- success = false if result.status != code.success
609
- end
610
- if success
611
- yield(:process, config) if block_given?
612
- extension(:exec_success, { :config => config, :results => results })
608
+ if results
609
+ success = true
610
+ results.each do |result|
611
+ success = false if result.status != code.success
612
+ end
613
+ if success
614
+ yield(:process, config) if block_given?
615
+ extension(:exec_success, { :config => config, :results => results })
616
+ end
617
+ else
618
+ default_error.append_errors("No execution results")
613
619
  end
620
+ else
621
+ default_error.append_errors("Execution prevented by exec hook")
614
622
  end
615
623
  else
624
+ default_error.append_errors("No attached machine")
625
+
616
626
  logger.warn("Node #{plugin_name} does not have an attached machine or is not running so cannot execute commands")
617
627
  end
618
628
  results
@@ -638,14 +648,14 @@ class Node < CORL.plugin_class(:base)
638
648
 
639
649
  admin_command = ''
640
650
  if as_admin
641
- admin_command = 'sudo' if user.to_s == 'ubuntu'
651
+ admin_command = 'sudo' if user.to_s != 'root'
642
652
  admin_command = extension_set(:admin_command, admin_command, config)
643
653
  end
644
654
 
645
655
  results = exec({ :commands => [ "#{admin_command} #{command.to_s}".strip ] }) do |op, data|
646
656
  yield(op, data) if block_given?
647
- end
648
- results.first
657
+ end
658
+ results.first
649
659
  end
650
660
 
651
661
  #---
@@ -737,6 +747,7 @@ class Node < CORL.plugin_class(:base)
737
747
  codes :local_path_not_found,
738
748
  :home_path_lookup_failure,
739
749
  :auth_upload_failure,
750
+ :root_auth_copy_failure,
740
751
  :bootstrap_upload_failure,
741
752
  :bootstrap_exec_failure,
742
753
  :reload_failure
@@ -748,7 +759,7 @@ class Node < CORL.plugin_class(:base)
748
759
  # Transmit authorisation / credential files
749
760
  package_files = [ '.fog', '.netrc', '.google-privatekey.p12', '.vimrc' ]
750
761
  auth_files.each do |file|
751
- package_files = file.gsub(local_path + '/', '')
762
+ package_files << file.gsub(local_path + '/', '')
752
763
  end
753
764
  send_success = send_files(local_path, user_home, package_files, '0600') do |op, data|
754
765
  yield("send_#{op}".to_sym, data) if block_given?
@@ -757,6 +768,14 @@ class Node < CORL.plugin_class(:base)
757
768
  unless send_success
758
769
  myself.status = code.auth_upload_failure
759
770
  end
771
+
772
+ if user.to_sym != config.get(:root_user, :root).to_sym
773
+ auth_files = package_files.collect { |path| "'#{path}'"}
774
+ root_home_path = config.get(:root_home, '/root')
775
+
776
+ result = command("cp #{auth_files.join(' ')} #{root_home_path}", { :as_admin => true })
777
+ myself.status = code.root_auth_copy_failure unless result.status == code.success
778
+ end
760
779
 
761
780
  # Send bootstrap package
762
781
  if status == code.success
@@ -809,16 +828,19 @@ class Node < CORL.plugin_class(:base)
809
828
  config = Config.ensure(options)
810
829
 
811
830
  # Record machine parameters
812
- id(true)
813
- public_ip(true)
814
- private_ip(true)
815
- state(true)
816
-
817
- machine_type(false)
818
- image(false)
831
+ if block_given?
832
+ # Provider or external configuration preparation
833
+ yield(config)
834
+ else
835
+ # Default configuration preparation
836
+ id(true)
837
+ public_ip(true)
838
+ private_ip(true)
839
+ state(true)
819
840
 
820
- # Provider or external configuration preparation
821
- yield(config) if block_given?
841
+ machine_type(false)
842
+ image(false)
843
+ end
822
844
 
823
845
  network.save(config.import({
824
846
  :commit => true,
@@ -943,15 +965,19 @@ class Node < CORL.plugin_class(:base)
943
965
 
944
966
  myself.machine = nil
945
967
 
946
- delete_setting(:id)
947
- delete_setting(:public_ip)
948
- delete_setting(:private_ip)
949
- delete_setting(:ssh_port)
950
- delete_setting(:build)
968
+ override_settings = false
969
+ override_settings = yield(:finalize, config) if success && block_given?
951
970
 
952
- myself[:state] = :stopped
971
+ if success && ! override_settings
972
+ delete_setting(:id)
973
+ delete_setting(:public_ip)
974
+ delete_setting(:private_ip)
975
+ delete_setting(:ssh_port)
976
+ delete_setting(:build)
953
977
 
954
- success = save(config) if success
978
+ myself[:state] = :stopped
979
+ end
980
+ success = save(config)
955
981
 
956
982
  if success && block_given?
957
983
  process_success = yield(:process, config)
@@ -1005,6 +1031,7 @@ class Node < CORL.plugin_class(:base)
1005
1031
 
1006
1032
  if success
1007
1033
  extension(:destroy_success, { :config => config })
1034
+ clear_cache
1008
1035
  end
1009
1036
  end
1010
1037
  else
@@ -1091,6 +1118,12 @@ class Node < CORL.plugin_class(:base)
1091
1118
  result.status == code.success ? true : false
1092
1119
  end
1093
1120
 
1121
+ #---
1122
+
1123
+ def filter_output(type, data)
1124
+ data
1125
+ end
1126
+
1094
1127
  #-----------------------------------------------------------------------------
1095
1128
  # Machine type utilities
1096
1129
 
@@ -4,8 +4,7 @@ module Plugin
4
4
  class Provisioner < CORL.plugin_class(:base)
5
5
 
6
6
  include Celluloid
7
- task_class TaskThread
8
-
7
+
9
8
  #-----------------------------------------------------------------------------
10
9
  # Provisioner plugin interface
11
10
 
@@ -0,0 +1,7 @@
1
+ #
2
+ # CORL Vagrant environment (development platform)
3
+ #-------------------------------------------------------------------------------
4
+ #
5
+ Vagrant.configure('2') do |config|
6
+ CORL.vagrant_config(File.dirname(__FILE__), config)
7
+ end
@@ -0,0 +1,66 @@
1
+
2
+ module VagrantPlugins
3
+ module CORL
4
+ module Command
5
+ class Launcher < ::Vagrant.plugin("2", :command)
6
+
7
+ include Celluloid
8
+
9
+ #-----------------------------------------------------------------------------
10
+
11
+ def self.synopsis
12
+ "execute CORL actions within the defined network"
13
+ end
14
+
15
+ #-----------------------------------------------------------------------------
16
+ # Property accessors / modifiers
17
+
18
+ def env
19
+ @env
20
+ end
21
+
22
+ #-----------------------------------------------------------------------------
23
+ # Execution
24
+
25
+ def execute
26
+ # Set the base command so we can access in any actions executed
27
+ ::CORL::Vagrant.command = current_actor
28
+ ::CORL.executable(@argv - [ "--" ], "vagrant corl")
29
+ end
30
+
31
+ #-----------------------------------------------------------------------------
32
+ # Utilities
33
+
34
+ def vm_machine(name, provider = nil, refresh = false)
35
+ machine = nil
36
+
37
+ # Mostly derived from Vagrant base command with_target_vms() method
38
+ provider = provider.to_sym if provider
39
+
40
+ env.active_machines.each do |active_name, active_provider|
41
+ if name == active_name
42
+ if provider && provider != active_provider
43
+ raise ::Vagrant::Errors::ActiveMachineWithDifferentProvider,
44
+ :name => active_name.to_s,
45
+ :active_provider => active_provider.to_s,
46
+ :requested_provider => provider.to_s
47
+ else
48
+ @logger.info("Active machine found with name #{active_name}. " +
49
+ "Using provider: #{active_provider}")
50
+ provider = active_provider
51
+ break
52
+ end
53
+ end
54
+ end
55
+
56
+ provider ||= env.default_provider
57
+
58
+ machine = env.machine(name, provider, refresh)
59
+ machine.ui.opts[:color] = :default # TODO: Something better??
60
+
61
+ machine
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end