sparkle_formation 1.2.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/bin/generate_sparkle_docs +21 -6
- data/docs/helper-methods.md +26 -49
- data/lib/sparkle_formation.rb +3 -0
- data/lib/sparkle_formation/error.rb +8 -0
- data/lib/sparkle_formation/function_struct.rb +123 -0
- data/lib/sparkle_formation/provider.rb +12 -0
- data/lib/sparkle_formation/provider/aws.rb +201 -0
- data/lib/sparkle_formation/provider/azure.rb +168 -0
- data/lib/sparkle_formation/provider/heat.rb +163 -0
- data/lib/sparkle_formation/resources.rb +27 -10
- data/lib/sparkle_formation/resources/azure.rb +42 -0
- data/lib/sparkle_formation/resources/azure_resources.json +353 -0
- data/lib/sparkle_formation/resources/heat.rb +39 -0
- data/lib/sparkle_formation/resources/heat_resources.json +4994 -0
- data/lib/sparkle_formation/resources/rackspace.rb +39 -0
- data/lib/sparkle_formation/resources/rackspace_resources.json +2561 -0
- data/lib/sparkle_formation/sparkle_attribute.rb +14 -19
- data/lib/sparkle_formation/sparkle_attribute/aws.rb +12 -12
- data/lib/sparkle_formation/sparkle_attribute/azure.rb +161 -0
- data/lib/sparkle_formation/sparkle_attribute/heat.rb +177 -0
- data/lib/sparkle_formation/sparkle_attribute/rackspace.rb +21 -0
- data/lib/sparkle_formation/sparkle_formation.rb +70 -154
- data/lib/sparkle_formation/sparkle_struct.rb +39 -2
- data/lib/sparkle_formation/version.rb +1 -1
- data/sparkle_formation.gemspec +7 -3
- metadata +63 -7
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'sparkle_formation'
|
2
|
+
|
3
|
+
class SparkleFormation
|
4
|
+
module Provider
|
5
|
+
# Azure specific implementation
|
6
|
+
module Azure
|
7
|
+
|
8
|
+
# @return [String] Type string for Azure Resource Manager stack resource
|
9
|
+
def stack_resource_type
|
10
|
+
'Microsoft.Resources/deployments'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Generate policy for stack
|
14
|
+
#
|
15
|
+
# @return [Hash]
|
16
|
+
def generate_policy
|
17
|
+
{}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Apply deeply nested stacks. This is the new nesting approach and
|
21
|
+
# does not bubble parameters up to the root stack. Parameters are
|
22
|
+
# isolated to the stack resource itself and output mapping is
|
23
|
+
# automatically applied.
|
24
|
+
#
|
25
|
+
# @yieldparam stack [SparkleFormation] stack instance
|
26
|
+
# @yieldparam resource [AttributeStruct] the stack resource
|
27
|
+
# @yieldparam s_name [String] stack resource name
|
28
|
+
# @yieldreturn [Hash] key/values to be merged into resource properties
|
29
|
+
# @return [Hash] dumped stack
|
30
|
+
def apply_deep_nesting(*args, &block)
|
31
|
+
outputs = collect_outputs
|
32
|
+
nested_stacks(:with_resource).each do |stack, resource|
|
33
|
+
unless(stack.nested_stacks.empty?)
|
34
|
+
stack.apply_deep_nesting(*args)
|
35
|
+
end
|
36
|
+
stack.compile.parameters.keys!.each do |parameter_name|
|
37
|
+
if(output_name = output_matched?(parameter_name, outputs.keys))
|
38
|
+
next if outputs[output_name] == stack
|
39
|
+
stack_output = stack.make_output_available(output_name, outputs)
|
40
|
+
resource.properties.parameters._set(parameter_name).value stack_output
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
if(block_given?)
|
45
|
+
extract_templates(&block)
|
46
|
+
end
|
47
|
+
compile.dump!
|
48
|
+
end
|
49
|
+
|
50
|
+
# Apply shallow nesting. This style of nesting will bubble
|
51
|
+
# parameters up to the root stack. This type of nesting is the
|
52
|
+
# original and now deprecated, but remains for compat issues so any
|
53
|
+
# existing usage won't be automatically busted.
|
54
|
+
#
|
55
|
+
# @yieldparam resource_name [String] name of stack resource
|
56
|
+
# @yieldparam stack [SparkleFormation] nested stack
|
57
|
+
# @yieldreturn [String] Remote URL storage for template
|
58
|
+
# @return [Hash]
|
59
|
+
def apply_shallow_nesting(*args, &block)
|
60
|
+
parameters = compile.parameters
|
61
|
+
output_map = {}
|
62
|
+
nested_stacks(:with_resource, :with_name).each do |_stack, stack_resource, stack_name|
|
63
|
+
remap_nested_parameters(compile, parameters, stack_name, stack_resource, output_map)
|
64
|
+
end
|
65
|
+
extract_templates(&block)
|
66
|
+
if(args.include?(:bubble_outputs))
|
67
|
+
output_map.each do |o_name, o_val|
|
68
|
+
compile.outputs._set(o_name).value compile._stack_output(*o_val)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
compile.dump!
|
72
|
+
end
|
73
|
+
|
74
|
+
# Extract output to make available for stack parameter usage at the
|
75
|
+
# current depth
|
76
|
+
#
|
77
|
+
# @param output_name [String] name of output
|
78
|
+
# @param outputs [Hash] listing of outputs
|
79
|
+
# @reutrn [Hash] reference to output value (used for setting parameter)
|
80
|
+
def make_output_available(output_name, outputs)
|
81
|
+
bubble_path = outputs[output_name].root_path - root_path
|
82
|
+
drip_path = root_path - outputs[output_name].root_path
|
83
|
+
bubble_path.each_slice(2) do |base_sparkle, ref_sparkle|
|
84
|
+
next unless ref_sparkle
|
85
|
+
base_sparkle.compile.outputs._set(output_name)._set(
|
86
|
+
:value, base_sparkle.compile._stack_output(
|
87
|
+
ref_sparkle.name, output_name
|
88
|
+
)
|
89
|
+
)
|
90
|
+
end
|
91
|
+
if(bubble_path.empty?)
|
92
|
+
if(drip_path.size == 1)
|
93
|
+
parent = drip_path.first.parent
|
94
|
+
if(parent && !parent.compile.parameters._set(output_name).nil?)
|
95
|
+
return compile.parameter!(output_name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
raise ArgumentError.new "Failed to detect available bubbling path for output `#{output_name}`. " <<
|
99
|
+
'This may be due to a circular dependency! ' <<
|
100
|
+
"(Output Path: #{outputs[output_name].root_path.map(&:name).join(' > ')} " <<
|
101
|
+
"Requester Path: #{root_path.map(&:name).join(' > ')})"
|
102
|
+
end
|
103
|
+
result = compile._stack_output(bubble_path.first.name, output_name)
|
104
|
+
if(drip_path.size > 1)
|
105
|
+
parent = drip_path.first.parent
|
106
|
+
drip_path.unshift(parent) if parent
|
107
|
+
drip_path.each_slice(2) do |base_sparkle, ref_sparkle|
|
108
|
+
next unless ref_sparkle
|
109
|
+
base_sparkle.compile.resources[ref_sparkle.name].properties.parameters.value._set(output_name, result)
|
110
|
+
ref_sparkle.compile.parameters._set(output_name).type 'string' # TODO: <<<<------ type check and prop
|
111
|
+
result = compile._parameter(output_name)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
# Extract parameters from nested stacks. Check for previous nested
|
118
|
+
# stack outputs that match parameter. If match, set parameter to use
|
119
|
+
# output. If no match, check container stack parameters for match.
|
120
|
+
# If match, set to use ref. If no match, add parameter to container
|
121
|
+
# stack parameters and set to use ref.
|
122
|
+
#
|
123
|
+
# @param template [Hash] template being processed
|
124
|
+
# @param parameters [Hash] top level parameter set being built
|
125
|
+
# @param stack_name [String] name of stack resource
|
126
|
+
# @param stack_resource [Hash] duplicate of stack resource contents
|
127
|
+
# @param output_map [Hash] mapping of output names to required stack output access
|
128
|
+
# @return [TrueClass]
|
129
|
+
# @note if parameter has includes `StackUnique` a new parameter will
|
130
|
+
# be added to container stack and it will not use outputs
|
131
|
+
def remap_nested_parameters(template, parameters, stack_name, stack_resource, output_map)
|
132
|
+
nested_template = stack_resource.properties.stack.compile
|
133
|
+
stack_parameters = nested_template.parameters
|
134
|
+
unless(stack_parameters.nil?)
|
135
|
+
stack_parameters._keys.each do |pname|
|
136
|
+
pval = stack_parameters[pname]
|
137
|
+
unless(pval.stack_unique.nil?)
|
138
|
+
check_name = [stack_name, pname].join
|
139
|
+
else
|
140
|
+
check_name = pname
|
141
|
+
end
|
142
|
+
if(!parameters._set(check_name).nil?)
|
143
|
+
template.resources._set(stack_name).properties.parameters._set(pname).value(
|
144
|
+
template._parameter(check_name)
|
145
|
+
)
|
146
|
+
elsif(output_map[check_name])
|
147
|
+
template.resources._set(stack_name).properties.parameters._set(pname).value(
|
148
|
+
template._stack_output(*output_map[check_name])
|
149
|
+
)
|
150
|
+
else
|
151
|
+
parameters._set(check_name, pval)
|
152
|
+
template.resources._set(stack_name).properties.parameters._set(pname).value(
|
153
|
+
template._parameter(check_name)
|
154
|
+
)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
unless(nested_template.outputs.nil?)
|
159
|
+
nested_template.outputs.keys!.each do |oname|
|
160
|
+
output_map[oname] = [stack_name, oname]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
true
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'sparkle_formation'
|
2
|
+
|
3
|
+
class SparkleFormation
|
4
|
+
module Provider
|
5
|
+
# Heat specific implementation
|
6
|
+
module Heat
|
7
|
+
|
8
|
+
# @return [String] Type string for OpenStack HEAT stack resource
|
9
|
+
def stack_resource_type
|
10
|
+
'OS::Heat::Stack'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Generate policy for stack
|
14
|
+
#
|
15
|
+
# @return [Hash]
|
16
|
+
def generate_policy
|
17
|
+
{}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Apply deeply nested stacks. This is the new nesting approach and
|
21
|
+
# does not bubble parameters up to the root stack. Parameters are
|
22
|
+
# isolated to the stack resource itself and output mapping is
|
23
|
+
# automatically applied.
|
24
|
+
#
|
25
|
+
# @yieldparam stack [SparkleFormation] stack instance
|
26
|
+
# @yieldparam resource [AttributeStruct] the stack resource
|
27
|
+
# @yieldparam s_name [String] stack resource name
|
28
|
+
# @yieldreturn [Hash] key/values to be merged into resource properties
|
29
|
+
# @return [Hash] dumped stack
|
30
|
+
def apply_deep_nesting(*args, &block)
|
31
|
+
outputs = collect_outputs
|
32
|
+
nested_stacks(:with_resource).each do |stack, resource|
|
33
|
+
unless(stack.nested_stacks.empty?)
|
34
|
+
stack.apply_deep_nesting(*args)
|
35
|
+
end
|
36
|
+
stack.compile.parameters.keys!.each do |parameter_name|
|
37
|
+
if(output_name = output_matched?(parameter_name, outputs.keys))
|
38
|
+
next if outputs[output_name] == stack
|
39
|
+
stack_output = stack.make_output_available(output_name, outputs)
|
40
|
+
resource.properties.parameters._set(parameter_name, stack_output)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
if(block_given?)
|
45
|
+
extract_templates(&block)
|
46
|
+
end
|
47
|
+
compile.dump!
|
48
|
+
end
|
49
|
+
|
50
|
+
# Apply shallow nesting. This style of nesting will bubble
|
51
|
+
# parameters up to the root stack. This type of nesting is the
|
52
|
+
# original and now deprecated, but remains for compat issues so any
|
53
|
+
# existing usage won't be automatically busted.
|
54
|
+
#
|
55
|
+
# @yieldparam resource_name [String] name of stack resource
|
56
|
+
# @yieldparam stack [SparkleFormation] nested stack
|
57
|
+
# @yieldreturn [String] Remote URL storage for template
|
58
|
+
# @return [Hash]
|
59
|
+
def apply_shallow_nesting(*args, &block)
|
60
|
+
parameters = compile.parameters
|
61
|
+
output_map = {}
|
62
|
+
nested_stacks(:with_resource, :with_name).each do |_stack, stack_resource, stack_name|
|
63
|
+
remap_nested_parameters(compile, parameters, stack_name, stack_resource, output_map)
|
64
|
+
end
|
65
|
+
extract_templates(&block)
|
66
|
+
if(args.include?(:bubble_outputs))
|
67
|
+
output_map.each do |o_name, o_val|
|
68
|
+
compile.outputs._set(o_name).value compile._stack_output(*o_val)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
compile.dump!
|
72
|
+
end
|
73
|
+
|
74
|
+
# Extract output to make available for stack parameter usage at the
|
75
|
+
# current depth
|
76
|
+
#
|
77
|
+
# @param output_name [String] name of output
|
78
|
+
# @param outputs [Hash] listing of outputs
|
79
|
+
# @reutrn [Hash] reference to output value (used for setting parameter)
|
80
|
+
def make_output_available(output_name, outputs)
|
81
|
+
bubble_path = outputs[output_name].root_path - root_path
|
82
|
+
drip_path = root_path - outputs[output_name].root_path
|
83
|
+
bubble_path.each_slice(2) do |base_sparkle, ref_sparkle|
|
84
|
+
next unless ref_sparkle
|
85
|
+
base_sparkle.compile.outputs._set(output_name)._set(
|
86
|
+
:value, base_sparkle.compile._stack_output(
|
87
|
+
ref_sparkle.name, output_name
|
88
|
+
)
|
89
|
+
)
|
90
|
+
end
|
91
|
+
if(bubble_path.empty?)
|
92
|
+
if(drip_path.size == 1)
|
93
|
+
parent = drip_path.first.parent
|
94
|
+
if(parent && !parent.compile.parameters._set(output_name).nil?)
|
95
|
+
return compile.parameter!(output_name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
raise ArgumentError.new "Failed to detect available bubbling path for output `#{output_name}`. " <<
|
99
|
+
'This may be due to a circular dependency! ' <<
|
100
|
+
"(Output Path: #{outputs[output_name].root_path.map(&:name).join(' > ')} " <<
|
101
|
+
"Requester Path: #{root_path.map(&:name).join(' > ')})"
|
102
|
+
end
|
103
|
+
result = compile._stack_output(bubble_path.first.name, output_name)
|
104
|
+
if(drip_path.size > 1)
|
105
|
+
parent = drip_path.first.parent
|
106
|
+
drip_path.unshift(parent) if parent
|
107
|
+
drip_path.each_slice(2) do |base_sparkle, ref_sparkle|
|
108
|
+
next unless ref_sparkle
|
109
|
+
base_sparkle.compile.resources[ref_sparkle.name].properties.parameters._set(output_name, result)
|
110
|
+
ref_sparkle.compile.parameters._set(output_name).type 'string' # TODO: <<<<------ type check and prop
|
111
|
+
result = compile._parameter(output_name)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
# Extract parameters from nested stacks. Check for previous nested
|
118
|
+
# stack outputs that match parameter. If match, set parameter to use
|
119
|
+
# output. If no match, check container stack parameters for match.
|
120
|
+
# If match, set to use ref. If no match, add parameter to container
|
121
|
+
# stack parameters and set to use ref.
|
122
|
+
#
|
123
|
+
# @param template [Hash] template being processed
|
124
|
+
# @param parameters [Hash] top level parameter set being built
|
125
|
+
# @param stack_name [String] name of stack resource
|
126
|
+
# @param stack_resource [Hash] duplicate of stack resource contents
|
127
|
+
# @param output_map [Hash] mapping of output names to required stack output access
|
128
|
+
# @return [TrueClass]
|
129
|
+
# @note if parameter has includes `StackUnique` a new parameter will
|
130
|
+
# be added to container stack and it will not use outputs
|
131
|
+
def remap_nested_parameters(template, parameters, stack_name, stack_resource, output_map)
|
132
|
+
nested_template = stack_resource.properties.stack.compile
|
133
|
+
stack_parameters = nested_template.parameters
|
134
|
+
unless(stack_parameters.nil?)
|
135
|
+
stack_parameters._keys.each do |pname|
|
136
|
+
pval = stack_parameters[pname]
|
137
|
+
unless(pval.stack_unique.nil?)
|
138
|
+
check_name = [stack_name, pname].join
|
139
|
+
else
|
140
|
+
check_name = pname
|
141
|
+
end
|
142
|
+
if(!parameters._set(check_name).nil?)
|
143
|
+
template.resources._set(stack_name).properties.parameters._set(pname, template._parameter(check_name))
|
144
|
+
elsif(output_map[check_name])
|
145
|
+
template.resources._set(stack_name).properties.parameters._set(pname)
|
146
|
+
template._stack_output(*output_map[check_name])
|
147
|
+
else
|
148
|
+
parameters._set(check_name, pval)
|
149
|
+
template.resources._set(stack_name).properties.parameters._set(pname, template._parameter(check_name))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
unless(nested_template.outputs.nil?)
|
154
|
+
nested_template.outputs.keys!.each do |oname|
|
155
|
+
output_map[oname] = [stack_name, oname]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
true
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -5,12 +5,25 @@ class SparkleFormation
|
|
5
5
|
class Resources
|
6
6
|
|
7
7
|
autoload :Aws, 'sparkle_formation/resources/aws'
|
8
|
+
autoload :Azure, 'sparkle_formation/resources/azure'
|
9
|
+
autoload :Heat, 'sparkle_formation/resources/heat'
|
10
|
+
autoload :Rackspace, 'sparkle_formation/resources/rackspace'
|
11
|
+
|
12
|
+
# Characters to be removed from supplied key on matching
|
13
|
+
RESOURCE_TYPE_TR = '_'
|
14
|
+
# String to split for resource namespacing
|
15
|
+
RESOURCE_TYPE_NAMESPACE_SPLITTER = '::'
|
8
16
|
|
9
17
|
class << self
|
10
18
|
|
11
19
|
include SparkleFormation::Utils::AnimalStrings
|
12
20
|
# @!parse include SparkleFormation::Utils::AnimalStrings
|
13
21
|
|
22
|
+
# @return [String] base registry key
|
23
|
+
def base_key
|
24
|
+
Bogo::Utility.snake(self.name.split('::').last) # rubocop:disable Style/RedundantSelf
|
25
|
+
end
|
26
|
+
|
14
27
|
# Register resource
|
15
28
|
#
|
16
29
|
# @param type [String] Orchestration resource type
|
@@ -20,7 +33,8 @@ class SparkleFormation
|
|
20
33
|
unless(class_variable_defined?(:@@registry))
|
21
34
|
@@registry = AttributeStruct.hashish.new
|
22
35
|
end
|
23
|
-
@@registry[
|
36
|
+
@@registry[base_key] ||= AttributeStruct.hashish.new
|
37
|
+
@@registry[base_key][type] = hash
|
24
38
|
true
|
25
39
|
end
|
26
40
|
|
@@ -67,11 +81,13 @@ class SparkleFormation
|
|
67
81
|
# @return [String, NilClass]
|
68
82
|
def registry_key(key)
|
69
83
|
o_key = key
|
70
|
-
key = key.to_s.tr(
|
84
|
+
key = key.to_s.tr(self.const_get(:RESOURCE_TYPE_TR), '') # rubocop:disable Style/RedundantSelf
|
71
85
|
snake_parts = nil
|
72
|
-
result = @@registry.keys.detect do |ref|
|
86
|
+
result = @@registry[base_key].keys.detect do |ref|
|
73
87
|
ref = ref.downcase
|
74
|
-
snake_parts = ref.split(
|
88
|
+
snake_parts = ref.split(
|
89
|
+
self.const_get(:RESOURCE_TYPE_NAMESPACE_SPLITTER) # rubocop:disable Style/RedundantSelf
|
90
|
+
)
|
75
91
|
until(snake_parts.empty?)
|
76
92
|
break if snake_parts.join('') == key
|
77
93
|
snake_parts.shift
|
@@ -79,8 +95,10 @@ class SparkleFormation
|
|
79
95
|
!snake_parts.empty?
|
80
96
|
end
|
81
97
|
if(result)
|
82
|
-
collisions = @@registry.keys.find_all do |ref|
|
83
|
-
split_ref = ref.downcase.split(
|
98
|
+
collisions = @@registry[base_key].keys.find_all do |ref|
|
99
|
+
split_ref = ref.downcase.split(
|
100
|
+
self.const_get(:RESOURCE_TYPE_NAMESPACE_SPLITTER) # rubocop:disable Style/RedundantSelf
|
101
|
+
)
|
84
102
|
ref = split_ref.slice(split_ref.size - snake_parts.size, split_ref.size).join('')
|
85
103
|
key == ref
|
86
104
|
end
|
@@ -97,16 +115,15 @@ class SparkleFormation
|
|
97
115
|
# @param key [String, Symbol]
|
98
116
|
# @return [Hashish, NilClass]
|
99
117
|
def lookup(key)
|
100
|
-
@@registry[registry_key(key)]
|
118
|
+
@@registry[base_key][registry_key(key)]
|
101
119
|
end
|
102
120
|
|
103
121
|
# @return [Hashish] currently loaded AWS registry
|
104
122
|
def registry
|
105
|
-
|
106
|
-
@@registry
|
107
|
-
else
|
123
|
+
unless(class_variable_defined?(:@@registry))
|
108
124
|
@@registry = AttributeStruct.hashish.new
|
109
125
|
end
|
126
|
+
@@registry[base_key]
|
110
127
|
end
|
111
128
|
|
112
129
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'sparkle_formation'
|
2
|
+
|
3
|
+
class SparkleFormation
|
4
|
+
|
5
|
+
# Resources helper
|
6
|
+
class Resources
|
7
|
+
|
8
|
+
# Azure specific resources collection
|
9
|
+
class Azure < Resources
|
10
|
+
|
11
|
+
# String to split for resource namespacing
|
12
|
+
RESOURCE_TYPE_NAMESPACE_SPLITTER = '/'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
include Bogo::Memoization
|
17
|
+
|
18
|
+
# Load the builtin AWS resources
|
19
|
+
#
|
20
|
+
# @return [TrueClass]
|
21
|
+
def load!
|
22
|
+
memoize(:azure_resources, :global) do
|
23
|
+
load(
|
24
|
+
File.join(
|
25
|
+
File.dirname(__FILE__),
|
26
|
+
'azure_resources.json'
|
27
|
+
)
|
28
|
+
)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Auto load data when included
|
34
|
+
def included(_klass)
|
35
|
+
load!
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|