sfn 0.0.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -0
- data/LICENSE +13 -0
- data/README.md +142 -61
- data/bin/sfn +43 -0
- data/lib/chef/knife/knife_plugin_seed.rb +117 -0
- data/lib/sfn.rb +17 -0
- data/lib/sfn/cache.rb +385 -0
- data/lib/sfn/command.rb +45 -0
- data/lib/sfn/command/create.rb +87 -0
- data/lib/sfn/command/describe.rb +87 -0
- data/lib/sfn/command/destroy.rb +74 -0
- data/lib/sfn/command/events.rb +98 -0
- data/lib/sfn/command/export.rb +103 -0
- data/lib/sfn/command/import.rb +117 -0
- data/lib/sfn/command/inspect.rb +160 -0
- data/lib/sfn/command/list.rb +59 -0
- data/lib/sfn/command/promote.rb +17 -0
- data/lib/sfn/command/update.rb +95 -0
- data/lib/sfn/command/validate.rb +34 -0
- data/lib/sfn/command_module.rb +9 -0
- data/lib/sfn/command_module/base.rb +150 -0
- data/lib/sfn/command_module/stack.rb +166 -0
- data/lib/sfn/command_module/template.rb +147 -0
- data/lib/sfn/config.rb +106 -0
- data/lib/sfn/config/create.rb +35 -0
- data/lib/sfn/config/describe.rb +19 -0
- data/lib/sfn/config/destroy.rb +9 -0
- data/lib/sfn/config/events.rb +25 -0
- data/lib/sfn/config/export.rb +29 -0
- data/lib/sfn/config/import.rb +24 -0
- data/lib/sfn/config/inspect.rb +37 -0
- data/lib/sfn/config/list.rb +25 -0
- data/lib/sfn/config/promote.rb +23 -0
- data/lib/sfn/config/update.rb +20 -0
- data/lib/sfn/config/validate.rb +49 -0
- data/lib/sfn/monkey_patch.rb +8 -0
- data/lib/sfn/monkey_patch/stack.rb +200 -0
- data/lib/sfn/provider.rb +224 -0
- data/lib/sfn/utils.rb +23 -0
- data/lib/sfn/utils/debug.rb +31 -0
- data/lib/sfn/utils/json.rb +37 -0
- data/lib/sfn/utils/object_storage.rb +28 -0
- data/lib/sfn/utils/output.rb +79 -0
- data/lib/sfn/utils/path_selector.rb +99 -0
- data/lib/sfn/utils/ssher.rb +29 -0
- data/lib/sfn/utils/stack_exporter.rb +275 -0
- data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
- data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
- data/lib/sfn/version.rb +4 -0
- data/sfn.gemspec +19 -0
- 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
|