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.
@@ -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
- attr_reader :original, :translated, :template, :logger
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
- def initialize(template_hash, logger=nil)
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
- if(logger)
20
- @logger = logger
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
- def translate_resources(value)
48
- translated['Resources'] = {}.tap do |modified_resources|
49
- value.each do |resource_name, resource_args|
50
- new_resource = {}
51
- lookup = map[:resources][resource_args['Type']]
52
- unless(lookup)
53
- logger.warn "Failed to locate resource type: #{resource_args['Type']}"
54
- next
55
- end
56
- new_resource['Type'] = lookup[:name]
57
- new_resource['Properties'] = {}.tap do |new_properties|
58
- resource_args['Properties'].each do |property_name, property_value|
59
- new_key = lookup[:properties][property_name]
60
- if(new_key)
61
- if(new_key.is_a?(Symbol))
62
- new_key, new_value = send(new_key, property_value,
63
- :new_resource => new_resource,
64
- :new_properties => new_properties,
65
- :original_resource => resource_args
66
- )
67
- new_properties[new_key] = new_value
68
- else
69
- new_properties[new_key] = property_value
70
- end
71
- else
72
- logger.warn "Failed to locate property conversion for `#{property_name}` on resource type `#{resource_args['Type']}`. Passing directly."
73
- new_properties[snake(property_name)] = property_value
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
- if(lookup[:finalizer])
78
- send(lookup[:finalizer], resource_name, new_resource, resource_args, modified_resources)
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
- # TODO: implement
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
- def nova_server_finalizer(*_)
17
- true
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
- def resource_finalizer(resource_name, new_resource, old_resource, translated_resources)
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
- # TODO: implement
30
- def autoscaling_group_resource(value, args={})
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' => :autoscaling_group_resource
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
- def nova_server_finalizer(resource_name, new_resource, old_resource, translated_resources)
9
- if(old_resource['Metadata'])
10
- new_resource['Metadata'] = old_resource['Metadata']
11
- if(new_resource['Metadata'] && new_resource['Metadata']['AWS::CloudFormation::Init'] && config = new_resource['Metadata']['AWS::CloudFormation::Init']['config'])
12
- # NOTE: This is a stupid hack since HOT gives the URL to
13
- # wget directly and if special characters exist, it fails
14
- if(files = config['files'])
15
- files.each do |key, args|
16
- if(args['source'])
17
- args['source'].replace("\"#{args['source']}\"")
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