corl 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitmodules +1 -1
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +15 -8
  4. data/VERSION +1 -1
  5. data/bootstrap/os/ubuntu/00_base.sh +10 -1
  6. data/bootstrap/os/ubuntu/05_ruby.sh +6 -0
  7. data/bootstrap/os/ubuntu/06_puppet.sh +6 -6
  8. data/bootstrap/os/ubuntu/09_nucleon.sh +14 -0
  9. data/bootstrap/os/ubuntu/10_corl.sh +7 -2
  10. data/corl.gemspec +16 -9
  11. data/lib/CORL/action/authorize.rb +57 -0
  12. data/lib/CORL/action/bootstrap.rb +5 -0
  13. data/lib/CORL/action/destroy.rb +64 -0
  14. data/lib/CORL/action/exec.rb +9 -0
  15. data/lib/CORL/action/image.rb +39 -7
  16. data/lib/CORL/action/images.rb +4 -3
  17. data/lib/CORL/action/lookup.rb +2 -2
  18. data/lib/CORL/action/regions.rb +51 -0
  19. data/lib/CORL/action/seed.rb +1 -1
  20. data/lib/CORL/action/spawn.rb +8 -9
  21. data/lib/CORL/action/ssh.rb +74 -0
  22. data/lib/CORL/action/start.rb +37 -5
  23. data/lib/CORL/action/stop.rb +37 -5
  24. data/lib/CORL/configuration/file.rb +34 -7
  25. data/lib/CORL/event/puppet.rb +1 -1
  26. data/lib/CORL/machine/aws.rb +153 -0
  27. data/lib/CORL/machine/physical.rb +14 -5
  28. data/lib/CORL/machine/rackspace.rb +58 -0
  29. data/lib/CORL/network/default.rb +1 -1
  30. data/lib/CORL/node/aws.rb +40 -16
  31. data/lib/CORL/node/local.rb +4 -3
  32. data/lib/CORL/node/rackspace.rb +25 -7
  33. data/lib/CORL/provisioner/puppetnode.rb +11 -9
  34. data/lib/core/errors.rb +6 -0
  35. data/lib/core/mod/fog_aws_server.rb +38 -0
  36. data/lib/core/plugin/action.rb +3 -11
  37. data/lib/core/plugin/configuration.rb +20 -2
  38. data/lib/{CORL/machine/fog.rb → core/plugin/fog_machine.rb} +92 -92
  39. data/lib/core/plugin/{fog.rb → fog_node.rb} +20 -7
  40. data/lib/core/plugin/machine.rb +58 -37
  41. data/lib/core/plugin/network.rb +76 -111
  42. data/lib/core/plugin/node.rb +271 -87
  43. data/lib/core/plugin/provisioner.rb +1 -1
  44. data/lib/corl.rb +6 -14
  45. data/locales/en.yml +18 -1
  46. metadata +39 -32
  47. data/lib/CORL/node/google.rb +0 -111
  48. data/lib/core/util/ssh.rb +0 -286
@@ -0,0 +1,74 @@
1
+
2
+ module CORL
3
+ module Action
4
+ class Ssh < Plugin::CloudAction
5
+
6
+ #-----------------------------------------------------------------------------
7
+ # Settings
8
+
9
+ def configure
10
+ super do
11
+ codes :network_failure
12
+
13
+ register :ssh_nodes, :array, nil do |values|
14
+ if values.nil?
15
+ warn('corl.actions.bootstrap.errors.ssh_nodes_empty')
16
+ next false
17
+ end
18
+
19
+ node_plugins = CORL.loaded_plugins(:node)
20
+ success = true
21
+
22
+ values.each do |value|
23
+ if info = CORL.plugin_class(:node).translate_reference(value)
24
+ if ! node_plugins.keys.include?(info[:provider].to_sym) || info[:name].empty?
25
+ warn('corl.actions.bootstrap.errors.ssh_nodes', { :value => value, :node_provider => info[:provider], :name => info[:name] })
26
+ success = false
27
+ end
28
+ end
29
+ end
30
+ success
31
+ end
32
+
33
+ config[:node_provider].default = :rackspace
34
+ end
35
+ end
36
+
37
+ #---
38
+
39
+ def ignore
40
+ node_ignore - [ :net_provider, :node_provider ]
41
+ end
42
+
43
+ def arguments
44
+ [ :ssh_nodes ]
45
+ end
46
+
47
+ #-----------------------------------------------------------------------------
48
+ # Operations
49
+
50
+ def execute
51
+ super do |local_node, network|
52
+ if network
53
+ batch_success = network.batch(settings[:ssh_nodes], settings[:node_provider], false) do |node|
54
+ render_options = { :id => node.id, :hostname => node.hostname }
55
+
56
+ info('corl.actions.ssh.start', render_options)
57
+ success = node.terminal(extended_config(:ssh, {}))
58
+ if success
59
+ info('corl.actions.ssh.success', render_options)
60
+ else
61
+ render_options[:status] = node.status
62
+ error('corl.actions.ssh.failure', render_options)
63
+ end
64
+ success
65
+ end
66
+ myself.status = code.batch_error unless batch_success
67
+ else
68
+ myself.status = code.network_failure
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -9,18 +9,50 @@ class Start < Plugin::CloudAction
9
9
  def configure
10
10
  super do
11
11
  codes :network_failure
12
+
13
+ register :start_nodes, :array, nil do |values|
14
+ if values.nil?
15
+ warn('corl.actions.start.errors.start_nodes_empty')
16
+ next false
17
+ end
18
+
19
+ node_plugins = CORL.loaded_plugins(:node)
20
+ success = true
21
+
22
+ values.each do |value|
23
+ if info = CORL.plugin_class(:node).translate_reference(value)
24
+ if ! node_plugins.keys.include?(info[:provider].to_sym) || info[:name].empty?
25
+ warn('corl.actions.start.errors.start_nodes', { :value => value, :node_provider => info[:provider], :name => info[:name] })
26
+ success = false
27
+ end
28
+ end
29
+ end
30
+ success
31
+ end
12
32
  end
13
33
  end
14
34
 
35
+ #---
36
+
37
+ def ignore
38
+ [ :nodes ]
39
+ end
40
+
41
+ def arguments
42
+ [ :start_nodes ]
43
+ end
44
+
15
45
  #-----------------------------------------------------------------------------
16
46
  # Operations
17
47
 
18
48
  def execute
19
- super do |node, network|
20
- info('corl.actions.start.start')
21
-
22
- if network && node
23
-
49
+ super do |local_node, network|
50
+ if network
51
+ batch_success = network.batch(settings[:start_nodes], settings[:node_provider], settings[:parallel]) do |node|
52
+ info('corl.actions.start.start', { :provider => node.plugin_provider, :name => node.plugin_name })
53
+ node.start
54
+ end
55
+ myself.status = code.batch_error unless batch_success
24
56
  else
25
57
  myself.status = code.network_failure
26
58
  end
@@ -9,18 +9,50 @@ class Stop < Plugin::CloudAction
9
9
  def configure
10
10
  super do
11
11
  codes :network_failure
12
+
13
+ register :stop_nodes, :array, nil do |values|
14
+ if values.nil?
15
+ warn('corl.actions.stop.errors.stop_nodes_empty')
16
+ next false
17
+ end
18
+
19
+ node_plugins = CORL.loaded_plugins(:node)
20
+ success = true
21
+
22
+ values.each do |value|
23
+ if info = CORL.plugin_class(:node).translate_reference(value)
24
+ if ! node_plugins.keys.include?(info[:provider].to_sym) || info[:name].empty?
25
+ warn('corl.actions.stop.errors.stop_nodes', { :value => value, :node_provider => info[:provider], :name => info[:name] })
26
+ success = false
27
+ end
28
+ end
29
+ end
30
+ success
31
+ end
12
32
  end
13
33
  end
14
34
 
35
+ #---
36
+
37
+ def ignore
38
+ [ :nodes ]
39
+ end
40
+
41
+ def arguments
42
+ [ :stop_nodes ]
43
+ end
44
+
15
45
  #-----------------------------------------------------------------------------
16
46
  # Operations
17
47
 
18
48
  def execute
19
- super do |node, network|
20
- info('corl.actions.stop.start')
21
-
22
- if network && node
23
-
49
+ super do |local_node, network|
50
+ if network
51
+ batch_success = network.batch(settings[:stop_nodes], settings[:node_provider], settings[:parallel]) do |node|
52
+ info('corl.actions.stop.start', { :provider => node.plugin_provider, :name => node.plugin_name })
53
+ node.stop
54
+ end
55
+ myself.status = code.batch_error unless batch_success
24
56
  else
25
57
  myself.status = code.network_failure
26
58
  end
@@ -6,7 +6,7 @@ class File < CORL.plugin_class(:configuration)
6
6
  #-----------------------------------------------------------------------------
7
7
  # Configuration plugin interface
8
8
 
9
- def normalize
9
+ def normalize(reload)
10
10
  super
11
11
 
12
12
  logger.info("Setting source configuration project")
@@ -151,6 +151,7 @@ class File < CORL.plugin_class(:configuration)
151
151
  else
152
152
  # Never encountered before
153
153
  config_name = nil
154
+
154
155
  config_name = select_largest(router.get(parents)) unless parents.empty?
155
156
  split_config.call(value, config_name, keys)
156
157
  end
@@ -165,12 +166,12 @@ class File < CORL.plugin_class(:configuration)
165
166
  file_data.set([ config_name, keys ].flatten, value)
166
167
  else
167
168
  # Router is non existent
168
- if config_name = select_largest(router)
169
+ if config_name = select_largest(router.export)
169
170
  # Pick largest router from top level
170
171
  file_data.set([ config_name, keys ].flatten, value)
171
172
  else
172
173
  # Resort to sane defaults
173
- default_provider = Manager.connection.type_default(:translator)
174
+ default_provider = CORL.type_default(:translator)
174
175
  config_name = "corl.#{default_provider}"
175
176
  file_data.set([ config_name, keys ].flatten, value)
176
177
  end
@@ -180,7 +181,6 @@ class File < CORL.plugin_class(:configuration)
180
181
  end
181
182
 
182
183
  # Whew! Glad that's over...
183
-
184
184
  split_config.call(config.export, router.export)
185
185
  file_data
186
186
  end
@@ -254,8 +254,8 @@ class File < CORL.plugin_class(:configuration)
254
254
  if success
255
255
  case method_config.get(:type, :source)
256
256
  when :source
257
- new_file = project.local_path(Util::Disk.filename([ attach_path, name ]))
258
-
257
+ new_file = project.local_path(Util::Disk.filename([ attach_path, name ]))
258
+
259
259
  logger.debug("Attaching source data (length: #{data.length}) to configuration at #{attach_path}")
260
260
  success = Util::Disk.write(new_file, data)
261
261
 
@@ -276,7 +276,7 @@ class File < CORL.plugin_class(:configuration)
276
276
  end
277
277
  end
278
278
  end
279
- if success
279
+ if success && autosave
280
280
  logger.debug("Attaching data to project as #{new_file}")
281
281
  success = update_project(new_file, method_config)
282
282
  end
@@ -284,6 +284,31 @@ class File < CORL.plugin_class(:configuration)
284
284
  end
285
285
  end
286
286
 
287
+ #---
288
+
289
+ def delete_attachments(ids, options = {})
290
+ super do |method_config|
291
+ success = true
292
+ files = []
293
+
294
+ array(ids).each do |id|
295
+ file = ::File.join(project.directory, id.to_s)
296
+
297
+ if Util::Disk.delete(file)
298
+ files << file
299
+ else
300
+ success = false
301
+ end
302
+ end
303
+
304
+ if success && autosave
305
+ logger.debug("Removing attached data from project as #{files.join(', ')}")
306
+ success = update_project(files, method_config)
307
+ end
308
+ success ? files : nil
309
+ end
310
+ end
311
+
287
312
  #-----------------------------------------------------------------------------
288
313
  # Utilities
289
314
 
@@ -354,6 +379,8 @@ class File < CORL.plugin_class(:configuration)
354
379
  #---
355
380
 
356
381
  def select_largest(router)
382
+ return router unless router.is_a?(Hash)
383
+
357
384
  config_map = {}
358
385
 
359
386
  count_config_names = lambda do |data|
@@ -6,7 +6,7 @@ class Puppet < CORL.plugin_class(:event)
6
6
  #-----------------------------------------------------------------------------
7
7
  # Puppet event interface
8
8
 
9
- def normalize
9
+ def normalize(reload)
10
10
  super
11
11
 
12
12
  if get(:string)
@@ -0,0 +1,153 @@
1
+
2
+ module CORL
3
+ module Machine
4
+ class Aws < Fog
5
+
6
+ #-----------------------------------------------------------------------------
7
+ # Checks
8
+
9
+ #-----------------------------------------------------------------------------
10
+ # Property accessors / modifiers
11
+
12
+ def set_connection
13
+ require 'unf'
14
+ super
15
+ Kernel.load File.join(File.dirname(__FILE__), '..', '..', 'core', 'mod', 'fog_aws_server.rb')
16
+ end
17
+
18
+ #-----------------------------------------------------------------------------
19
+ # Management
20
+
21
+ def init_server
22
+ super do
23
+ myself.plugin_name = @server.id
24
+
25
+ node[:id] = plugin_name
26
+ node[:public_ip] = @server.public_ip_address
27
+ node[:private_ip] = @server.private_ip_address
28
+ node[:machine_type] = @server.flavor_id
29
+ node[:image] = @server.image_id
30
+ node.user = @server.username unless node.user
31
+
32
+ @server.private_key_path = node.private_key if node.private_key
33
+ @server.public_key_path = node.public_key if node.public_key
34
+ end
35
+ end
36
+
37
+ #---
38
+
39
+ def init_ssh(ssh_port)
40
+ # Security group initialization
41
+ if compute && ssh_port != 22
42
+ ensure_security_group("CORL_SSH_#{ssh_port}", ssh_port)
43
+ end
44
+ end
45
+
46
+ #---
47
+
48
+ def create(options = {})
49
+ super do |config|
50
+ # Keypair initialization
51
+ if key_pair = compute.key_pairs.get(keypair_name)
52
+ key_pair.destroy
53
+ end
54
+ compute.key_pairs.create(
55
+ :name => keypair_name,
56
+ :public_key => Util::Disk.read(node.public_key)
57
+ )
58
+ config[:key_name] = keypair_name
59
+ end
60
+ end
61
+
62
+ #---
63
+
64
+ def reload(options = {})
65
+ super do |config|
66
+ success = server.reboot
67
+
68
+ server.wait_for { ready? } if success
69
+ success
70
+ end
71
+ end
72
+
73
+ #---
74
+
75
+ def create_image(options = {})
76
+ super do |image_name, config, success|
77
+ image_name = image_name.gsub(/[^A-Za-z0-9\(\)\.\-\_\/]+/, '_')
78
+ image_description = config.get(:description, "CORL backup image")
79
+
80
+ data = compute.create_image(server.identity, image_name, image_description)
81
+ image_id = data.body['imageId']
82
+
83
+ ::Fog.wait_for do
84
+ compute.describe_images('ImageId' => image_id).body['imagesSet'].first['imageState'] == 'available'
85
+ end
86
+
87
+ if image_id
88
+ node[:image] = image_id
89
+ success = true
90
+ end
91
+ success
92
+ end
93
+ end
94
+
95
+ #---
96
+
97
+ def destroy(options = {})
98
+ super do |config|
99
+ unless config.get(:stop, false)
100
+ # Keypair destruction
101
+ if key_pair = compute.key_pairs.get(keypair_name)
102
+ key_pair.destroy
103
+ end
104
+ end
105
+ true
106
+ end
107
+ end
108
+
109
+ #-----------------------------------------------------------------------------
110
+ # Utilities
111
+
112
+ def keypair_name
113
+ "CORL_#{node.plugin_name}"
114
+ end
115
+
116
+ #---
117
+
118
+ def ensure_security_group(group_name, from_port, to_port = nil, options = {})
119
+ config = Config.ensure(options)
120
+ security_group = compute.security_groups.get(group_name)
121
+ cidrip = config.get(:cidrip, '0.0.0.0/0')
122
+ protocol = config.get(:protocol, 'tcp')
123
+ to_port = from_port if to_port.nil?
124
+
125
+ if security_group.nil?
126
+ security_group = compute.security_groups.create(
127
+ :name => group_name,
128
+ :description => config.get(:description, "Opening port range: #{from_port} to #{to_port}")
129
+ )
130
+ raise unless security_group # TODO: Better error class
131
+ end
132
+
133
+ authorized = false
134
+ if security_group.ip_permissions
135
+ authorized = security_group.ip_permissions.detect do |ip_permission|
136
+ ip_permission['ipRanges'].first && ip_permission['ipRanges'].first['cidrIp'] == cidrip &&
137
+ ip_permission['fromPort'] == from_port &&
138
+ ip_permission['ipProtocol'] == protocol &&
139
+ ip_permission['toPort'] == to_port
140
+ end
141
+ end
142
+ unless authorized
143
+ security_group.authorize_port_range(Range.new(from_port, to_port))
144
+ end
145
+
146
+ if server
147
+ server.groups = [ group_name ] | server.groups
148
+ server.save
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -6,7 +6,7 @@ class Physical < CORL.plugin_class(:machine)
6
6
  #-----------------------------------------------------------------------------
7
7
  # Machine plugin interface
8
8
 
9
- def normalize
9
+ def normalize(reload)
10
10
  super
11
11
  myself.plugin_name = hostname
12
12
  end
@@ -40,7 +40,7 @@ class Physical < CORL.plugin_class(:machine)
40
40
  #---
41
41
 
42
42
  def public_ip
43
- fact(:ipaddress)
43
+ CORL.ip_address
44
44
  end
45
45
 
46
46
  #---
@@ -99,13 +99,13 @@ class Physical < CORL.plugin_class(:machine)
99
99
 
100
100
  #---
101
101
 
102
- def exec(commands, options = {})
102
+ def exec(commands, options = {}, &code)
103
103
  super do |config, results|
104
- logger.debug("Executing shell commands ( #{commands.inspect} ) on machine #{name}")
104
+ logger.debug("Executing shell commands ( #{commands.inspect} ) on machine #{plugin_name}")
105
105
 
106
106
  commands.each do |command|
107
107
  result = CORL.cli_run(command, config) do |op, command_str, data|
108
- block_given? ? yield(op, command_str, data) : true
108
+ code ? code.call(op, command_str, data) : true
109
109
  end
110
110
  results << result
111
111
  end
@@ -115,6 +115,15 @@ class Physical < CORL.plugin_class(:machine)
115
115
 
116
116
  #---
117
117
 
118
+ def terminal(user, options = {})
119
+ super do |config|
120
+ logger.debug("Launching terminals on the local machine is not currently supported")
121
+ 1
122
+ end
123
+ end
124
+
125
+ #---
126
+
118
127
  def start(options = {})
119
128
  super do
120
129
  logger.warn("This machine is already running so can not be started")
@@ -0,0 +1,58 @@
1
+
2
+ module CORL
3
+ module Machine
4
+ class Rackspace < Fog
5
+
6
+ #-----------------------------------------------------------------------------
7
+ # Checks
8
+
9
+ #-----------------------------------------------------------------------------
10
+ # Property accessors / modifiers
11
+
12
+ #-----------------------------------------------------------------------------
13
+ # Management
14
+
15
+ def init_server
16
+ super do
17
+ myself.plugin_name = @server.id
18
+
19
+ node[:id] = plugin_name
20
+ node[:public_ip] = @server.public_ip_address
21
+ node[:private_ip] = @server.private_ip_address
22
+ node[:machine_type] = @server.flavor.id
23
+ node[:image] = @server.image.id
24
+ node.user = @server.username unless node.user
25
+
26
+ @server.private_key_path = node.private_key if node.private_key
27
+ @server.public_key_path = node.public_key if node.public_key
28
+ end
29
+ end
30
+
31
+ #---
32
+
33
+ def reload(options = {})
34
+ super do |config|
35
+ success = server.reboot(config.get(:type, 'SOFT'))
36
+
37
+ server.wait_for { ready? } if success
38
+ success
39
+ end
40
+ end
41
+
42
+ #---
43
+
44
+ def create_image(options = {})
45
+ super do |image_name, config, success|
46
+ image = server.create_image(image_name)
47
+ image.wait_for { ready? }
48
+
49
+ if image
50
+ node[:image] = image.id
51
+ success = true
52
+ end
53
+ success
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -6,7 +6,7 @@ class Default < CORL.plugin_class(:network)
6
6
  #-----------------------------------------------------------------------------
7
7
  # Cloud plugin interface
8
8
 
9
- def normalize
9
+ def normalize(reload)
10
10
  super
11
11
  end
12
12
 
data/lib/CORL/node/aws.rb CHANGED
@@ -1,11 +1,17 @@
1
1
 
2
2
  module CORL
3
3
  module Node
4
- class Aws < Node::Fog
4
+ class Aws < Fog
5
5
 
6
6
  #-----------------------------------------------------------------------------
7
7
  # Node plugin interface
8
-
8
+
9
+ def normalize(reload)
10
+ super do
11
+ :aws
12
+ end
13
+ end
14
+
9
15
  #-----------------------------------------------------------------------------
10
16
  # Checks
11
17
 
@@ -18,9 +24,14 @@ class Aws < Node::Fog
18
24
 
19
25
  def regions
20
26
  [
21
- 'us-west-2a',
22
- 'us-west-2b',
23
- 'us-west-2c'
27
+ 'us-east-1',
28
+ 'us-west-1',
29
+ 'us-west-2',
30
+ 'eu-west-1',
31
+ 'ap-northeast-1',
32
+ 'ap-southeast-1',
33
+ 'ap-southeast-2',
34
+ 'sa-east-1'
24
35
  ]
25
36
  end
26
37
 
@@ -30,7 +41,8 @@ class Aws < Node::Fog
30
41
  def machine_config
31
42
  super do |config|
32
43
  config.import({
33
- :provider => 'AWS'
44
+ :provider => 'AWS',
45
+ :region => region
34
46
  })
35
47
 
36
48
  config[:aws_access_key_id] = api_user if api_user
@@ -38,24 +50,33 @@ class Aws < Node::Fog
38
50
  end
39
51
  end
40
52
 
53
+ #---
54
+
55
+ def create_config
56
+ { :flavor_id => machine_type, :image_id => image, :username => user }
57
+ end
58
+
41
59
  #-----------------------------------------------------------------------------
42
60
  # Node operations
43
61
 
44
62
  def create(options = {})
45
63
  super do |op, config|
46
64
  if op == :config
47
- config[:private_key] = private_key if private_key
48
- config[:public_key] = public_key if public_key
49
-
50
- config.defaults({
51
- :name => hostname,
52
- :flavor_id => machine_type,
53
- :image_id => image
54
- })
65
+ config.defaults(create_config)
55
66
  end
56
67
  end
57
68
  end
58
69
 
70
+ #---
71
+
72
+ def start(options = {})
73
+ super do |op, config|
74
+ if op == :config
75
+ config.defaults(create_config)
76
+ end
77
+ end
78
+ end
79
+
59
80
  #-----------------------------------------------------------------------------
60
81
  # Utilities
61
82
 
@@ -73,13 +94,16 @@ class Aws < Node::Fog
73
94
  #---
74
95
 
75
96
  def render_image(image)
76
- sprintf("[ %20s ][ %10s ] %10s - %s", image_id(image), image.state, image.architecture, image.name)
97
+ location = image.location.split('/').first
98
+ sprintf("[ %20s ][ %10s ] %10s - %s (%s)", image_id(image), image.state, image.architecture, image.name, location)
77
99
  end
78
100
 
79
101
  #---
80
102
 
81
103
  def image_search_text(image)
82
- sprintf("%s %s %s %s %s", image_id(image), image.name, image.description, image.state, image.architecture)
104
+ location = image.location.split('/').first
105
+ location = location.match(/^\d+$/) ? '' : location
106
+ sprintf("%s %s %s %s %s %s %s", image_id(image), image.name, image.description, image.state, image.architecture, image.owner_id, location)
83
107
  end
84
108
  end
85
109
  end
@@ -6,9 +6,10 @@ class Local < CORL.plugin_class(:node)
6
6
  #-----------------------------------------------------------------------------
7
7
  # Node plugin interface
8
8
 
9
- def normalize
10
- super
11
- myself.machine = create_machine(:machine, :physical, machine_config)
9
+ def normalize(reload)
10
+ super do
11
+ myself.machine = create_machine(:machine, :physical, machine_config)
12
+ end
12
13
  end
13
14
 
14
15
  #-----------------------------------------------------------------------------