sparkle_formation 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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