sfn 0.0.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -0
  3. data/LICENSE +13 -0
  4. data/README.md +142 -61
  5. data/bin/sfn +43 -0
  6. data/lib/chef/knife/knife_plugin_seed.rb +117 -0
  7. data/lib/sfn.rb +17 -0
  8. data/lib/sfn/cache.rb +385 -0
  9. data/lib/sfn/command.rb +45 -0
  10. data/lib/sfn/command/create.rb +87 -0
  11. data/lib/sfn/command/describe.rb +87 -0
  12. data/lib/sfn/command/destroy.rb +74 -0
  13. data/lib/sfn/command/events.rb +98 -0
  14. data/lib/sfn/command/export.rb +103 -0
  15. data/lib/sfn/command/import.rb +117 -0
  16. data/lib/sfn/command/inspect.rb +160 -0
  17. data/lib/sfn/command/list.rb +59 -0
  18. data/lib/sfn/command/promote.rb +17 -0
  19. data/lib/sfn/command/update.rb +95 -0
  20. data/lib/sfn/command/validate.rb +34 -0
  21. data/lib/sfn/command_module.rb +9 -0
  22. data/lib/sfn/command_module/base.rb +150 -0
  23. data/lib/sfn/command_module/stack.rb +166 -0
  24. data/lib/sfn/command_module/template.rb +147 -0
  25. data/lib/sfn/config.rb +106 -0
  26. data/lib/sfn/config/create.rb +35 -0
  27. data/lib/sfn/config/describe.rb +19 -0
  28. data/lib/sfn/config/destroy.rb +9 -0
  29. data/lib/sfn/config/events.rb +25 -0
  30. data/lib/sfn/config/export.rb +29 -0
  31. data/lib/sfn/config/import.rb +24 -0
  32. data/lib/sfn/config/inspect.rb +37 -0
  33. data/lib/sfn/config/list.rb +25 -0
  34. data/lib/sfn/config/promote.rb +23 -0
  35. data/lib/sfn/config/update.rb +20 -0
  36. data/lib/sfn/config/validate.rb +49 -0
  37. data/lib/sfn/monkey_patch.rb +8 -0
  38. data/lib/sfn/monkey_patch/stack.rb +200 -0
  39. data/lib/sfn/provider.rb +224 -0
  40. data/lib/sfn/utils.rb +23 -0
  41. data/lib/sfn/utils/debug.rb +31 -0
  42. data/lib/sfn/utils/json.rb +37 -0
  43. data/lib/sfn/utils/object_storage.rb +28 -0
  44. data/lib/sfn/utils/output.rb +79 -0
  45. data/lib/sfn/utils/path_selector.rb +99 -0
  46. data/lib/sfn/utils/ssher.rb +29 -0
  47. data/lib/sfn/utils/stack_exporter.rb +275 -0
  48. data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
  49. data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
  50. data/lib/sfn/version.rb +4 -0
  51. data/sfn.gemspec +19 -0
  52. metadata +110 -4
@@ -0,0 +1,29 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module Utils
5
+
6
+ # Helper methods for SSH interactions
7
+ module Ssher
8
+
9
+ # Retrieve file from remote node
10
+ #
11
+ # @param address [String]
12
+ # @param user [String]
13
+ # @param path [String] remote file path
14
+ # @param ssh_opts [Hash]
15
+ # @return [String, NilClass]
16
+ def remote_file_contents(address, user, path, ssh_opts={})
17
+ if(path.to_s.strip.empty?)
18
+ raise ArgumentError.new 'No file path provided!'
19
+ end
20
+ require 'net/ssh'
21
+ content = ''
22
+ ssh_session = Net::SSH.start(address, user, ssh_opts)
23
+ content = ssh_session.exec!("sudo cat #{path}")
24
+ content.empty? ? nil : content
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,275 @@
1
+ begin
2
+ require 'chef'
3
+ rescue LoadError
4
+ $stderr.puts "WARN: Failed to load Chef. Chef specific features will be disabled!"
5
+ end
6
+ require 'sfn'
7
+
8
+ module Sfn
9
+ module Utils
10
+
11
+ # Stack serialization helper
12
+ class StackExporter
13
+
14
+ include Bogo::AnimalStrings
15
+ include Sfn::Utils::JSON
16
+
17
+ # default chef environment name
18
+ DEFAULT_CHEF_ENVIRONMENT = '_default'
19
+ # default instance options
20
+ DEFAULT_OPTIONS = Mash.new(
21
+ :chef_popsicle => true,
22
+ :ignored_parameters => ['Environment', 'StackCreator', 'Creator'],
23
+ :chef_environment_parameter => 'Environment'
24
+ )
25
+ # default structure of export payload
26
+ DEFAULT_EXPORT_STRUCTURE = {
27
+ :stack => Mash.new(
28
+ :template => nil,
29
+ :options => {
30
+ :parameters => Mash.new,
31
+ :capabilities => [],
32
+ :notification_topics => []
33
+ }
34
+ ),
35
+ :generator => {
36
+ :timestamp => Time.now.to_i,
37
+ :name => 'SparkleFormation',
38
+ :version => Sfn::VERSION.version,
39
+ :provider => nil
40
+ }
41
+ }
42
+
43
+ # @return [Miasma::Models::Orchestration::Stack]
44
+ attr_reader :stack
45
+ # @return [Hash]
46
+ attr_reader :options
47
+ # @return [Hash]
48
+ attr_reader :stack_export
49
+
50
+ # Create new instance
51
+ #
52
+ # @param stack [Miasma::Models::Orchestration::Stack]
53
+ # @param options [Hash]
54
+ # @option options [KnifeCloudformation::Provider] :provider
55
+ # @option options [TrueClass, FalseClass] :chef_popsicle freeze run list
56
+ # @option options [Array<String>] :ignored_parameters
57
+ # @option options [String] :chef_environment_parameter
58
+ def initialize(stack, options={})
59
+ @stack = stack
60
+ @options = DEFAULT_OPTIONS.merge(options)
61
+ @stack_export = Smash.new
62
+ end
63
+
64
+ # Export stack
65
+ #
66
+ # @return [Hash] exported stack
67
+ def export
68
+ @stack_export = Smash.new(DEFAULT_EXPORT_STRUCTURE).tap do |stack_export|
69
+ [:parameters, :capabilities, :notification_topics].each do |key|
70
+ if(val = stack.send(key))
71
+ stack_export[:stack][key] = val
72
+ end
73
+ end
74
+ stack_export[:stack][:template] = stack.template
75
+ stack_export[:generator][:timestamp] = Time.now.to_i
76
+ stack_export[:generator][:provider] = stack.provider.connection.provider
77
+ if(chef_popsicle? && defined?(Chef))
78
+ freeze_runlists(stack_export)
79
+ end
80
+ remove_ignored_parameters(stack_export)
81
+ stack_export[:stack][:template] = _to_json(
82
+ stack_export[:stack][:template]
83
+ )
84
+ end
85
+ end
86
+
87
+ # Provide query methods on options hash
88
+ #
89
+ # @param args [Object] argument list
90
+ # @return [Object]
91
+ def method_missing(*args)
92
+ m = args.first.to_s
93
+ if(m.end_with?('?') && options.has_key?(k = m.sub('?', '').to_sym))
94
+ !!options[k]
95
+ else
96
+ super
97
+ end
98
+ end
99
+
100
+ protected
101
+
102
+ # Remove parameter values from export that are configured to be
103
+ # ignored
104
+ #
105
+ # @param export [Hash] stack export
106
+ # @return [Hash]
107
+ def remove_ignored_parameters(export)
108
+ options[:ignored_parameters].each do |param|
109
+ if(export[:stack][:options][:parameters])
110
+ export[:stack][:options][:parameters].delete(param)
111
+ end
112
+ end
113
+ export
114
+ end
115
+
116
+ # Environment name to use when interacting with Chef
117
+ #
118
+ # @param export [Hash] current export state
119
+ # @return [String] environment name
120
+ def chef_environment_name(export)
121
+ if(chef_environment_parameter?)
122
+ name = export[:stack][:options][:parameters][options[:chef_environment_parameter]]
123
+ end
124
+ name || DEFAULT_CHEF_ENVIRONMENT
125
+ end
126
+
127
+ # @return [Chef::Environment]
128
+ def environment
129
+ unless(@env)
130
+ @env = Chef::Environment.load('_default')
131
+ end
132
+ @env
133
+ end
134
+
135
+ # Find latest available cookbook version within
136
+ # the configured environment
137
+ #
138
+ # @param cookbook [String] name of cookbook
139
+ # @return [Chef::Version]
140
+ def allowed_cookbook_version(cookbook)
141
+ restriction = environment.cookbook_versions[cookbook]
142
+ requirement = Gem::Requirement.new(restriction)
143
+ Chef::CookbookVersion.available_versions(cookbook).detect do |v|
144
+ requirement.satisfied_by?(Gem::Version.new(v))
145
+ end
146
+ end
147
+
148
+ # Extract the runlist item. Fully expands roles and provides
149
+ # version pegged runlist.
150
+ #
151
+ # @param item [Chef::RunList::RunListItem, Array<String>]
152
+ # @return [Hash] new chef configuration hash
153
+ # @note this will expand all roles
154
+ def extract_runlist_item(item)
155
+ rl_item = item.is_a?(Chef::RunList::RunListItem) ? item : Chef::RunList::RunListItem.new(item)
156
+ static_content = Mash.new(:run_list => [])
157
+ if(rl_item.recipe?)
158
+ cookbook, recipe = rl_item.name.split('::')
159
+ peg_version = allowed_cookbook_version(cookbook)
160
+ static_content[:run_list] << "recipe[#{[cookbook, recipe || 'default'].join('::')}@#{peg_version}]"
161
+ elsif(rl_item.role?)
162
+ role = Chef::Role.load(rl_item.name)
163
+ role.run_list.each do |item|
164
+ static_content = Chef::Mixin::DeepMerge.merge(static_content, extract_runlist_item(item))
165
+ end
166
+ static_content = Chef::Mixin::DeepMerge.merge(
167
+ static_content, Chef::Mixin::DeepMerge.merge(role.default_attributes, role.override_attributes)
168
+ )
169
+ else
170
+ raise TypeError.new("Unknown chef run list item encountered: #{rl_item.inspect}")
171
+ end
172
+ static_content
173
+ end
174
+
175
+ # Expand any detected chef run lists and freeze them within the
176
+ # stack template
177
+ #
178
+ # @param first_run [Hash] chef first run hash
179
+ # @return [Hash]
180
+ def unpack_and_freeze_runlist(first_run)
181
+ extracted_runlists = first_run['run_list'].map do |item|
182
+ extract_runlist_item(cf_replace(item))
183
+ end
184
+ first_run.delete('run_list')
185
+ first_run.replace(
186
+ extracted_runlists.inject(first_run) do |memo, first_run_item|
187
+ Chef::Mixin::DeepMerge.merge(memo, first_run_item)
188
+ end
189
+ )
190
+ end
191
+
192
+ # Freeze chef run lists
193
+ #
194
+ # @param exported [Hash] stack export
195
+ # @return [Hash]
196
+ def freeze_runlists(exported)
197
+ first_runs = locate_runlists(exported)
198
+ first_runs.each do |first_run|
199
+ unpack_and_freeze_runlist(first_run)
200
+ end
201
+ exported
202
+ end
203
+
204
+ # Locate chef run lists within data collection
205
+ #
206
+ # @param thing [Enumerable] collection from export
207
+ # @return [Enumerable] updated collection from export
208
+ def locate_runlists(thing)
209
+ result = []
210
+ case thing
211
+ when Hash
212
+ if(thing['content'] && thing['content']['run_list'])
213
+ result << thing['content']
214
+ else
215
+ thing.each do |k,v|
216
+ result += locate_runlists(v)
217
+ end
218
+ end
219
+ when Array
220
+ thing.each do |v|
221
+ result += locate_runlists(v)
222
+ end
223
+ end
224
+ result
225
+ end
226
+
227
+ # Apply cloudformation function to data
228
+ #
229
+ # @param hsh [Object] stack template item
230
+ # @return [Object]
231
+ def cf_replace(hsh)
232
+ if(hsh.is_a?(Hash))
233
+ case hsh.keys.first
234
+ when 'Fn::Join'
235
+ cf_join(*hsh.values.first)
236
+ when 'Ref'
237
+ cf_ref(hsh.values.first)
238
+ else
239
+ hsh
240
+ end
241
+ else
242
+ hsh
243
+ end
244
+ end
245
+
246
+ # Apply Ref function
247
+ #
248
+ # @param ref_name [Hash]
249
+ # @return [Object] value in parameters
250
+ def cf_ref(ref_name)
251
+ if(stack.parameters.has_key?(ref_name))
252
+ stack.parameters[ref_name]
253
+ else
254
+ raise KeyError.new("No parameter found with given reference name (#{ref_name}). " <<
255
+ "Only parameter based references supported!")
256
+ end
257
+ end
258
+
259
+ # Apply Join function
260
+ #
261
+ # @param delim [String] join delimiter
262
+ # @param args [String, Hash] items to join
263
+ # @return [String]
264
+ def cf_join(delim, args)
265
+ args.map do |arg|
266
+ if(arg.is_a?(Hash))
267
+ cf_replace(arg)
268
+ else
269
+ arg.to_s
270
+ end
271
+ end.join(delim)
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,37 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module Utils
5
+ # Helper for scrubbing stack parameters
6
+ class StackParameterScrubber
7
+
8
+ class << self
9
+
10
+ # Validate attributes within Parameter blocks
11
+ ALLOWED_PARAMETER_ATTRIBUTES = %w(
12
+ Type Default NoEcho AllowedValues AllowedPattern
13
+ MaxLength MinLength MaxValue MinValue Description
14
+ ConstraintDescription
15
+ )
16
+
17
+ # Clean the parameters of the template
18
+ #
19
+ # @param template [Hash]
20
+ # @return [Hash] template
21
+ def scrub!(template)
22
+ parameters = template['Parameters']
23
+ if(parameters)
24
+ parameters.each do |name, options|
25
+ options.delete_if do |attribute, value|
26
+ !ALLOWED_PARAMETER_ATTRIBUTES.include?(attribute)
27
+ end
28
+ end
29
+ template['Parameters'] = parameters
30
+ end
31
+ template
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,124 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ module Utils
5
+
6
+ # Helper utility for validating stack parameters
7
+ class StackParameterValidator
8
+ class << self
9
+
10
+ include Bogo::AnimalStrings
11
+
12
+ # Validate a parameters
13
+ #
14
+ # @param value [Object] value for parameter
15
+ # @param parameter_definition [Hash]
16
+ # @option parameter_definition [Array<String>] 'AllowedValues'
17
+ # @option parameter_definition [String] 'AllowedPattern'
18
+ # @option parameter_definition [String, Integer] 'MaxLength'
19
+ # @option parameter_definition [String, Integer] 'MinLength'
20
+ # @option parameter_definition [String, Integer] 'MaxValue'
21
+ # @option parameter_definition [String, Integer] 'MinValue'
22
+ # @return [TrueClass, Array<String>] true if valid. array of string errors if invalid
23
+ def validate(value, parameter_definition)
24
+ return [[:blank, 'Value cannot be blank']] if value.to_s.strip.empty?
25
+ result = %w(AllowedValues AllowedPattern MaxLength MinLength MaxValue MinValue).map do |key|
26
+ if(parameter_definition[key])
27
+ res = self.send(snake(key), value, parameter_definition)
28
+ res == true ? true : [snake(key), res]
29
+ else
30
+ true
31
+ end
32
+ end
33
+ result.delete_if{|x| x == true}
34
+ result.empty? ? true : result
35
+ end
36
+
37
+ # Parameter is within allowed values
38
+ #
39
+ # @param value [String]
40
+ # @param pdef [Hash] parameter definition
41
+ # @option pdef [Array<String>] 'AllowedValues'
42
+ # @return [TrueClass, String]
43
+ def allowed_values(value, pdef)
44
+ if(pdef['AllowedValues'].include?(value))
45
+ true
46
+ else
47
+ "Not an allowed value: #{pdef['AllowedValues'].join(', ')}"
48
+ end
49
+ end
50
+
51
+ # Parameter matches allowed pattern
52
+ #
53
+ # @param value [String]
54
+ # @param pdef [Hash] parameter definition
55
+ # @option pdef [String] 'AllowedPattern'
56
+ # @return [TrueClass, String]
57
+ def allowed_pattern(value, pdef)
58
+ if(value.match(/#{pdef['AllowedPattern']}/))
59
+ true
60
+ else
61
+ "Not a valid pattern. Must match: #{pdef['AllowedPattern']}"
62
+ end
63
+ end
64
+
65
+ # Parameter length is less than or equal to max length
66
+ #
67
+ # @param value [String, Integer]
68
+ # @param pdef [Hash] parameter definition
69
+ # @option pdef [String] 'MaxLength'
70
+ # @return [TrueClass, String]
71
+ def max_length(value, pdef)
72
+ if(value.length <= pdef['MaxLength'].to_i)
73
+ true
74
+ else
75
+ "Value must not exceed #{pdef['MaxLength']} characters"
76
+ end
77
+ end
78
+
79
+ # Parameter length is greater than or equal to min length
80
+ #
81
+ # @param value [String]
82
+ # @param pdef [Hash] parameter definition
83
+ # @option pdef [String] 'MinLength'
84
+ # @return [TrueClass, String]
85
+ def min_length(value, pdef)
86
+ if(value.length >= pdef['MinLength'].to_i)
87
+ true
88
+ else
89
+ "Value must be at least #{pdef['MinLength']} characters"
90
+ end
91
+ end
92
+
93
+ # Parameter value is less than or equal to max value
94
+ #
95
+ # @param value [String]
96
+ # @param pdef [Hash] parameter definition
97
+ # @option pdef [String] 'MaxValue'
98
+ # @return [TrueClass, String]
99
+ def max_value(value, pdef)
100
+ if(value.to_i <= pdef['MaxValue'].to_i)
101
+ true
102
+ else
103
+ "Value must not be greater than #{pdef['MaxValue']}"
104
+ end
105
+ end
106
+
107
+ # Parameter value is greater than or equal to min value
108
+ #
109
+ # @param value [String]
110
+ # @param pdef [Hash] parameter definition
111
+ # @option pdef [String] 'MinValue'
112
+ # @return [TrueClass, String]
113
+ def min_value(value, pdef)
114
+ if(value.to_i >= pdef['MinValue'].to_i)
115
+ true
116
+ else
117
+ "Value must not be less than #{pdef['MinValue']}"
118
+ end
119
+ end
120
+
121
+ end
122
+ end
123
+ end
124
+ end