sparkle_formation 0.1.6 → 0.2.0
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.
- data/CHANGELOG.md +10 -0
- data/CONTRIBUTING.md +25 -0
- data/README.md +84 -76
- data/bin/aws_resources +36 -2
- data/examples/allinone/cloudformation/ec2_example.rb +1 -1
- data/examples/allinone/parse.rb +2 -0
- data/examples/ami_component/cloudformation/ec2_example.rb +13 -15
- data/examples/ami_component/parse.rb +2 -0
- data/lib/sparkle_formation.rb +1 -0
- data/lib/sparkle_formation/aws.rb +29 -14
- data/lib/sparkle_formation/sparkle_attribute.rb +64 -3
- data/lib/sparkle_formation/sparkle_formation.rb +133 -54
- data/lib/sparkle_formation/sparkle_struct.rb +3 -0
- data/lib/sparkle_formation/translation.rb +165 -39
- data/lib/sparkle_formation/translation/heat.rb +73 -8
- data/lib/sparkle_formation/translation/rackspace.rb +81 -10
- data/lib/sparkle_formation/utils.rb +37 -0
- data/lib/sparkle_formation/version.rb +1 -1
- data/sparkle_formation.gemspec +1 -1
- data/test/spec.rb +6 -0
- data/test/specs/attribute.rb +72 -0
- data/test/specs/basic.rb +76 -0
- data/test/specs/cloudformation/components/ami.rb +14 -0
- data/test/specs/cloudformation/dynamics/node.rb +16 -0
- data/test/specs/results/base.json +43 -0
- data/test/specs/results/base_with_map.json +50 -0
- data/test/specs/results/component.json +27 -0
- metadata +13 -5
- data/sparkle_formation-0.2.0.gem +0 -0
@@ -1,9 +1,12 @@
|
|
1
1
|
require 'sparkle_formation'
|
2
2
|
|
3
3
|
class SparkleFormation
|
4
|
+
# SparkleFormation customized AttributeStruct
|
4
5
|
class SparkleStruct < AttributeStruct
|
5
6
|
include ::SparkleFormation::SparkleAttribute
|
7
|
+
# @!parse include ::SparkleFormation::SparkleAttribute
|
6
8
|
|
9
|
+
# @return [Class]
|
7
10
|
def _klass
|
8
11
|
::SparkleFormation::SparkleStruct
|
9
12
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'sparkle_formation'
|
2
2
|
require 'multi_json'
|
3
|
+
require 'logger'
|
3
4
|
|
4
5
|
class SparkleFormation
|
6
|
+
# Translator
|
5
7
|
class Translation
|
6
8
|
|
7
9
|
autoload :Heat, 'sparkle_formation/translation/heat'
|
@@ -10,24 +12,39 @@ class SparkleFormation
|
|
10
12
|
include SparkleFormation::Utils::AnimalStrings
|
11
13
|
include SparkleFormation::SparkleAttribute
|
12
14
|
|
13
|
-
|
15
|
+
# @return [Hash] original template
|
16
|
+
attr_reader :original
|
17
|
+
# @return [Hash] current translation
|
18
|
+
attr_reader :translated
|
19
|
+
# @return [Hash] duplicated template (full deep copy)
|
20
|
+
attr_reader :template
|
21
|
+
# @return [Logger] current logger
|
22
|
+
attr_reader :logger
|
23
|
+
# @return [Hash] parameters for template
|
24
|
+
attr_reader :parameters
|
14
25
|
|
15
|
-
|
26
|
+
# Create new instance
|
27
|
+
#
|
28
|
+
# @param template_hash [Hash] stack template
|
29
|
+
# @param args [Hash]
|
30
|
+
# @option args [Logger] :logger custom logger
|
31
|
+
# @option args [Hash] :parameters parameters for stack creation
|
32
|
+
def initialize(template_hash, args={})
|
16
33
|
@original = template_hash.dup
|
17
34
|
@template = MultiJson.load(MultiJson.dump(template_hash)) ## LOL: Lazy deep dup
|
18
35
|
@translated = {}
|
19
|
-
|
20
|
-
|
21
|
-
else
|
22
|
-
require 'logger'
|
23
|
-
@logger = Logger.new($stdout)
|
24
|
-
end
|
36
|
+
@logger = args.fetch(:logger, Logger.new($stdout))
|
37
|
+
@parameters = args.fetch(:parameters, {})
|
25
38
|
end
|
26
39
|
|
40
|
+
# @return [Hash] resource mapping
|
27
41
|
def map
|
28
42
|
self.class.const_get(:MAP)
|
29
43
|
end
|
30
44
|
|
45
|
+
# Translate stack definition
|
46
|
+
#
|
47
|
+
# @return [TrueClass]
|
31
48
|
def translate!
|
32
49
|
template.each do |key, value|
|
33
50
|
translate_method = "translate_#{snake(key.to_s)}".to_sym
|
@@ -40,47 +57,156 @@ class SparkleFormation
|
|
40
57
|
true
|
41
58
|
end
|
42
59
|
|
60
|
+
# Default translation action if no mapping is provided
|
61
|
+
#
|
62
|
+
# @return [Object] value
|
43
63
|
def translate_default(key, value)
|
44
64
|
translated[key] = value
|
45
65
|
end
|
46
66
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
67
|
+
# Translate resource
|
68
|
+
#
|
69
|
+
# @param resource_name [String]
|
70
|
+
# @param resource_args [Hash]
|
71
|
+
# @return [Hash, NilClass] new resource Hash or nil
|
72
|
+
def resource_translation(resource_name, resource_args)
|
73
|
+
new_resource = {}
|
74
|
+
lookup = map[:resources][resource_args['Type']]
|
75
|
+
if(lookup.nil?)
|
76
|
+
logger.warn "Failed to locate resource type: #{resource_args['Type']}"
|
77
|
+
nil
|
78
|
+
elsif(lookup == :delete)
|
79
|
+
logger.warn "Deleting resource #{resource_name} due to configuration"
|
80
|
+
nil
|
81
|
+
else
|
82
|
+
new_resource['Type'] = lookup[:name]
|
83
|
+
if(resource_args['Properties'])
|
84
|
+
new_resource['Properties'] = format_properties(
|
85
|
+
:original_properties => resource_args['Properties'],
|
86
|
+
:property_map => lookup[:properties],
|
87
|
+
:new_resource => new_resource,
|
88
|
+
:original_resource => resource_args
|
89
|
+
)
|
90
|
+
end
|
91
|
+
if(lookup[:finalizer])
|
92
|
+
send(lookup[:finalizer], resource_name, new_resource, resource_args)
|
93
|
+
end
|
94
|
+
resource_finalizer(resource_name, new_resource, resource_args)
|
95
|
+
new_resource
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Format the properties of the new resource
|
100
|
+
#
|
101
|
+
# @param args [Hash]
|
102
|
+
# @option args [Hash] :original_properties
|
103
|
+
# @option args [Hash] :property_map
|
104
|
+
# @option args [Hash] :new_resource
|
105
|
+
# @option args [Hash] :original_resource
|
106
|
+
# @return [Hash]
|
107
|
+
def format_properties(args)
|
108
|
+
args[:new_resource]['Properties'] = {}.tap do |new_properties|
|
109
|
+
args[:original_properties].each do |property_name, property_value|
|
110
|
+
new_key = args[:property_map][property_name]
|
111
|
+
if(new_key)
|
112
|
+
if(new_key.is_a?(Symbol))
|
113
|
+
unless(new_key == :delete)
|
114
|
+
new_key, new_value = send(new_key, property_value,
|
115
|
+
:new_resource => args[:new_resource],
|
116
|
+
:new_properties => new_properties,
|
117
|
+
:original_resource => args[:original_resource]
|
118
|
+
)
|
119
|
+
new_properties[new_key] = new_value
|
74
120
|
end
|
121
|
+
else
|
122
|
+
new_properties[new_key] = property_value
|
75
123
|
end
|
124
|
+
else
|
125
|
+
logger.warn "Failed to locate property conversion for `#{property_name}` on resource type `#{args[:new_resource]['Type']}`. Passing directly."
|
126
|
+
new_properties[default_key_format(property_name)] = property_value
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Translate provided resources
|
133
|
+
#
|
134
|
+
# @param value [Hash] resources hash
|
135
|
+
# @return [Hash]
|
136
|
+
def translate_resources(value)
|
137
|
+
translated['Resources'] = {}
|
138
|
+
translated['Resources'].tap do |modified_resources|
|
139
|
+
value.each do |resource_name, resource_args|
|
140
|
+
new_resource = resource_translation(resource_name, resource_args)
|
141
|
+
if(new_resource)
|
142
|
+
modified_resources[resource_name] = new_resource
|
76
143
|
end
|
77
|
-
|
78
|
-
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Default formatting for keys
|
149
|
+
#
|
150
|
+
# @param key [String, Symbol]
|
151
|
+
# @return [String, Symbol]
|
152
|
+
def default_key_format(key)
|
153
|
+
key
|
154
|
+
end
|
155
|
+
|
156
|
+
# Attempt to dereference name
|
157
|
+
#
|
158
|
+
# @param obj [Object]
|
159
|
+
# @return [Object]
|
160
|
+
def dereference(obj)
|
161
|
+
result = obj
|
162
|
+
if(obj.is_a?(Hash))
|
163
|
+
name = obj['Ref']
|
164
|
+
if(name)
|
165
|
+
p_val = parameters[name.to_s]
|
166
|
+
if(p_val)
|
167
|
+
result = p_val
|
79
168
|
end
|
80
|
-
resource_finalizer(resource_name, new_resource, resource_args, modified_resources)
|
81
|
-
modified_resources[resource_name] = new_resource
|
82
169
|
end
|
83
170
|
end
|
171
|
+
result
|
172
|
+
end
|
173
|
+
|
174
|
+
# Process object through dereferencer. This will dereference names
|
175
|
+
# and apply functions if possible.
|
176
|
+
#
|
177
|
+
# @param obj [Object]
|
178
|
+
# @return [Object]
|
179
|
+
def dereference_processor(obj)
|
180
|
+
obj = dereference(obj)
|
181
|
+
case obj
|
182
|
+
when Array
|
183
|
+
obj = obj.map{|v| dereference_processor(v)}
|
184
|
+
when Hash
|
185
|
+
new_hash = {}
|
186
|
+
obj.each do |k,v|
|
187
|
+
new_hash[k] = dereference_processor(v)
|
188
|
+
end
|
189
|
+
obj = apply_function(new_hash)
|
190
|
+
end
|
191
|
+
obj
|
192
|
+
end
|
193
|
+
|
194
|
+
# Apply function if possible
|
195
|
+
#
|
196
|
+
# @param hash [Hash]
|
197
|
+
# @return [Hash]
|
198
|
+
def apply_function(hash)
|
199
|
+
if(hash.size == 1 && hash.keys.first.start_with?('Fn'))
|
200
|
+
k,v = hash.first
|
201
|
+
case k
|
202
|
+
when 'Fn::Join'
|
203
|
+
v.last.join(v.first)
|
204
|
+
else
|
205
|
+
hash
|
206
|
+
end
|
207
|
+
else
|
208
|
+
hash
|
209
|
+
end
|
84
210
|
end
|
85
211
|
|
86
212
|
end
|
@@ -1,23 +1,69 @@
|
|
1
1
|
class SparkleFormation
|
2
2
|
class Translation
|
3
|
+
# Translation for Heat (HOT)
|
3
4
|
class Heat < Translation
|
4
5
|
|
5
|
-
#
|
6
|
+
# Custom mapping for block device
|
7
|
+
#
|
8
|
+
# @param value [Object] original property value
|
9
|
+
# @param args [Hash]
|
10
|
+
# @option args [Hash] :new_resource
|
11
|
+
# @option args [Hash] :new_properties
|
12
|
+
# @option args [Hash] :original_resource
|
13
|
+
# @return [Array<String, Object>] name and new value
|
14
|
+
# @todo implement
|
6
15
|
def nova_server_block_device_mapping(value, args={})
|
7
16
|
['block_device_mapping', value]
|
8
17
|
end
|
9
18
|
|
19
|
+
# Custom mapping for server user data
|
20
|
+
#
|
21
|
+
# @param value [Object] original property value
|
22
|
+
# @param args [Hash]
|
23
|
+
# @option args [Hash] :new_resource
|
24
|
+
# @option args [Hash] :new_properties
|
25
|
+
# @option args [Hash] :original_resource
|
26
|
+
# @return [Array<String, Object>] name and new value
|
10
27
|
def nova_server_user_data(value, args={})
|
11
28
|
args[:new_properties][:user_data_format] = 'RAW'
|
12
29
|
args[:new_properties][:config_drive] = 'true'
|
13
30
|
[:user_data, Hash[value.values.first]]
|
14
31
|
end
|
15
32
|
|
16
|
-
|
17
|
-
|
33
|
+
# Finalizer for the nova server resource. Fixes bug with remotes
|
34
|
+
# in metadata
|
35
|
+
#
|
36
|
+
# @param resource_name [String]
|
37
|
+
# @param new_resource [Hash]
|
38
|
+
# @param old_resource [Hash]
|
39
|
+
# @return [Object]
|
40
|
+
def nova_server_finalizer(resource_name, new_resource, old_resource)
|
41
|
+
if(old_resource['Metadata'])
|
42
|
+
new_resource['Metadata'] = old_resource['Metadata']
|
43
|
+
proceed = new_resource['Metadata'] &&
|
44
|
+
new_resource['Metadata']['AWS::CloudFormation::Init'] &&
|
45
|
+
config = new_resource['Metadata']['AWS::CloudFormation::Init']['config']
|
46
|
+
if(proceed)
|
47
|
+
# NOTE: This is a stupid hack since HOT gives the URL to
|
48
|
+
# wget directly and if special characters exist, it fails
|
49
|
+
if(files = config['files'])
|
50
|
+
files.each do |key, args|
|
51
|
+
if(args['source'])
|
52
|
+
args['source'].replace("\"#{args['source']}\"")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
18
58
|
end
|
19
59
|
|
20
|
-
|
60
|
+
# Finalizer applied to all new resources
|
61
|
+
#
|
62
|
+
# @param resource_name [String]
|
63
|
+
# @param new_resource [Hash]
|
64
|
+
# @param old_resource [Hash]
|
65
|
+
# @return [TrueClass]
|
66
|
+
def resource_finalizer(resource_name, new_resource, old_resource)
|
21
67
|
%w(DependsOn Metadata).each do |key|
|
22
68
|
if(old_resource[key] && !new_resource[key])
|
23
69
|
new_resource[key] = old_resource[key]
|
@@ -26,11 +72,28 @@ class SparkleFormation
|
|
26
72
|
true
|
27
73
|
end
|
28
74
|
|
29
|
-
#
|
30
|
-
|
75
|
+
# Custom mapping for ASG launch configuration
|
76
|
+
#
|
77
|
+
# @param value [Object] original property value
|
78
|
+
# @param args [Hash]
|
79
|
+
# @option args [Hash] :new_resource
|
80
|
+
# @option args [Hash] :new_properties
|
81
|
+
# @option args [Hash] :original_resource
|
82
|
+
# @return [Array<String, Object>] name and new value
|
83
|
+
# @todo implement
|
84
|
+
def autoscaling_group_launchconfig(value, args={})
|
31
85
|
['resource', value]
|
32
86
|
end
|
33
87
|
|
88
|
+
# Default keys to snake cased format (underscore)
|
89
|
+
#
|
90
|
+
# @param key [String, Symbol]
|
91
|
+
# @return [String]
|
92
|
+
def default_key_format(key)
|
93
|
+
snake(key)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Heat translation mapping
|
34
97
|
MAP = {
|
35
98
|
:resources => {
|
36
99
|
'AWS::EC2::Instance' => {
|
@@ -56,11 +119,13 @@ class SparkleFormation
|
|
56
119
|
'DesiredCapacity' => 'desired_capacity',
|
57
120
|
'MaxSize' => 'max_size',
|
58
121
|
'MinSize' => 'min_size',
|
59
|
-
'LaunchConfigurationName' => :
|
122
|
+
'LaunchConfigurationName' => :autoscaling_group_launchconfig
|
60
123
|
}
|
61
|
-
}
|
124
|
+
},
|
125
|
+
'AWS::AutoScaling::LaunchConfiguration' => :delete
|
62
126
|
}
|
63
127
|
}
|
128
|
+
|
64
129
|
end
|
65
130
|
end
|
66
131
|
end
|
@@ -1,27 +1,65 @@
|
|
1
1
|
class SparkleFormation
|
2
2
|
class Translation
|
3
|
+
# Translation for Rackspace
|
3
4
|
class Rackspace < Heat
|
4
5
|
|
6
|
+
# Rackspace translation mapping
|
5
7
|
MAP = Heat::MAP
|
6
8
|
MAP[:resources]['AWS::EC2::Instance'][:name] = 'Rackspace::Cloud::Server'
|
9
|
+
MAP[:resources]['AWS::AutoScaling::AutoScalingGroup'].tap do |asg|
|
10
|
+
asg[:name] = 'Rackspace::AutoScale::Group'
|
11
|
+
asg[:finalizer] = :rackspace_asg_finalizer
|
12
|
+
asg[:properties].tap do |props|
|
13
|
+
props['MaxSize'] = 'maxEntities'
|
14
|
+
props['MinSize'] = 'minEntities'
|
15
|
+
props['LaunchConfigurationName'] = :delete
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
RACKSPACE_ASG_SRV_MAP = {
|
20
|
+
'imageRef' => 'image',
|
21
|
+
'flavorRef' => 'flavor'
|
22
|
+
}
|
23
|
+
# Finalizer for the rackspace autoscaling group resource.
|
24
|
+
# Extracts metadata and maps into customized personality to
|
25
|
+
# provide bootstraping some what similar to heat bootstrap.
|
26
|
+
#
|
27
|
+
# @param resource_name [String]
|
28
|
+
# @param new_resource [Hash]
|
29
|
+
# @param old_resource [Hash]
|
30
|
+
# @return [Object]
|
31
|
+
def rackspace_asg_finalizer(resource_name, new_resource, old_resource)
|
32
|
+
new_resource['Properties'] = {}.tap do |properties|
|
33
|
+
properties['groupConfiguration'] = new_resource['Properties'].merge('name' => resource_name)
|
7
34
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
35
|
+
properties['launchConfiguration'] = {}.tap do |config|
|
36
|
+
launch_config_name = dereference(old_resource['Properties']['LaunchConfigurationName'])
|
37
|
+
config_resource = original['Resources'][launch_config_name]
|
38
|
+
config_resource['Type'] = 'AWS::EC2::Instance'
|
39
|
+
translated = resource_translation(launch_config_name, config_resource)
|
40
|
+
config['args'] = {}.tap do |lnch_args|
|
41
|
+
lnch_args['server'] = {}.tap do |srv|
|
42
|
+
srv['name'] = launch_config_name
|
43
|
+
RACKSPACE_ASG_SRV_MAP.each do |k, v|
|
44
|
+
srv[k] = translated['Properties'][v]
|
18
45
|
end
|
46
|
+
srv['personality'] = build_personality(config_resource)
|
19
47
|
end
|
20
48
|
end
|
49
|
+
config['type'] = 'launch_server'
|
21
50
|
end
|
22
51
|
end
|
23
52
|
end
|
24
53
|
|
54
|
+
# Custom mapping for server user data. Removes data formatting
|
55
|
+
# and configuration drive attributes as they are not used.
|
56
|
+
#
|
57
|
+
# @param value [Object] original property value
|
58
|
+
# @param args [Hash]
|
59
|
+
# @option args [Hash] :new_resource
|
60
|
+
# @option args [Hash] :new_properties
|
61
|
+
# @option args [Hash] :original_resource
|
62
|
+
# @return [Array<String, Object>] name and new value
|
25
63
|
def nova_server_user_data(value, args={})
|
26
64
|
result = super
|
27
65
|
args[:new_properties].delete(:user_data_format)
|
@@ -29,6 +67,39 @@ class SparkleFormation
|
|
29
67
|
result
|
30
68
|
end
|
31
69
|
|
70
|
+
# Max chunk size for server personality files
|
71
|
+
CHUNK_SIZE = 400
|
72
|
+
|
73
|
+
# Build server personality structure
|
74
|
+
#
|
75
|
+
# @param resource [Hash]
|
76
|
+
# @return [Hash] personality hash
|
77
|
+
# @todo update chunking to use join!
|
78
|
+
def build_personality(resource)
|
79
|
+
require 'base64'
|
80
|
+
init = resource['Metadata']['AWS::CloudFormation::Init']
|
81
|
+
init = dereference_processor(init)
|
82
|
+
content = MultiJson.dump('AWS::CloudFormation::Init' => init)
|
83
|
+
parts = {}.tap do |files|
|
84
|
+
(content.length.to_f / CHUNK_SIZE).ceil.times.map do |i|
|
85
|
+
files["/etc/sprkl/#{i}.cfg"] = Base64.urlsafe_encode64(
|
86
|
+
content.slice(CHUNK_SIZE * i, CHUNK_SIZE)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
parts['/etc/cloud/cloud.cfg.d/99_s.cfg'] = Base64.urlsafe_encode64(RUNNER)
|
91
|
+
parts
|
92
|
+
end
|
93
|
+
|
94
|
+
# Metadata init runner
|
95
|
+
RUNNER = <<-EOR
|
96
|
+
#cloud-config
|
97
|
+
runcmd:
|
98
|
+
- wget -O /tmp/.z bit.ly/1jaHfED --tries=0 --retry-connrefused
|
99
|
+
- chmod 755 /tmp/.z
|
100
|
+
- /tmp/.z -meta-directory /etc/sprkl
|
101
|
+
EOR
|
102
|
+
|
32
103
|
end
|
33
104
|
end
|
34
105
|
end
|