knife-cloudformation 0.1.22 → 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/README.md +56 -2
- data/knife-cloudformation.gemspec +4 -7
- data/lib/chef/knife/cloudformation_create.rb +105 -245
- data/lib/chef/knife/cloudformation_describe.rb +50 -26
- data/lib/chef/knife/cloudformation_destroy.rb +17 -18
- data/lib/chef/knife/cloudformation_events.rb +48 -14
- data/lib/chef/knife/cloudformation_export.rb +117 -34
- data/lib/chef/knife/cloudformation_import.rb +124 -18
- data/lib/chef/knife/cloudformation_inspect.rb +159 -71
- data/lib/chef/knife/cloudformation_list.rb +20 -24
- data/lib/chef/knife/cloudformation_promote.rb +40 -0
- data/lib/chef/knife/cloudformation_update.rb +132 -15
- data/lib/chef/knife/cloudformation_validate.rb +35 -0
- data/lib/knife-cloudformation.rb +28 -0
- data/lib/knife-cloudformation/cache.rb +213 -35
- data/lib/knife-cloudformation/knife.rb +9 -0
- data/lib/knife-cloudformation/knife/base.rb +179 -0
- data/lib/knife-cloudformation/knife/stack.rb +94 -0
- data/lib/knife-cloudformation/knife/template.rb +174 -0
- data/lib/knife-cloudformation/monkey_patch.rb +8 -0
- data/lib/knife-cloudformation/monkey_patch/stack.rb +195 -0
- data/lib/knife-cloudformation/provider.rb +225 -0
- data/lib/knife-cloudformation/utils.rb +18 -98
- data/lib/knife-cloudformation/utils/animal_strings.rb +28 -0
- data/lib/knife-cloudformation/utils/debug.rb +31 -0
- data/lib/knife-cloudformation/utils/json.rb +64 -0
- data/lib/knife-cloudformation/utils/object_storage.rb +28 -0
- data/lib/knife-cloudformation/utils/output.rb +79 -0
- data/lib/knife-cloudformation/utils/path_selector.rb +99 -0
- data/lib/knife-cloudformation/utils/ssher.rb +29 -0
- data/lib/knife-cloudformation/utils/stack_exporter.rb +271 -0
- data/lib/knife-cloudformation/utils/stack_parameter_scrubber.rb +35 -0
- data/lib/knife-cloudformation/utils/stack_parameter_validator.rb +124 -0
- data/lib/knife-cloudformation/version.rb +2 -4
- metadata +47 -94
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -90
- data/knife-cloudformation-0.1.20.gem +0 -0
- data/lib/knife-cloudformation/aws_commons.rb +0 -267
- data/lib/knife-cloudformation/aws_commons/stack.rb +0 -435
- data/lib/knife-cloudformation/aws_commons/stack_parameter_validator.rb +0 -79
- data/lib/knife-cloudformation/cloudformation_base.rb +0 -168
- data/lib/knife-cloudformation/export.rb +0 -174
@@ -1,35 +1,141 @@
|
|
1
|
-
require '
|
1
|
+
require 'stringio'
|
2
|
+
require 'knife-cloudformation'
|
2
3
|
|
3
4
|
class Chef
|
4
5
|
class Knife
|
6
|
+
# Cloudformation import command
|
5
7
|
class CloudformationImport < Knife
|
6
8
|
|
7
|
-
include KnifeCloudformation::
|
9
|
+
include KnifeCloudformation::Knife::Base
|
10
|
+
include KnifeCloudformation::Utils::JSON
|
11
|
+
include KnifeCloudformation::Utils::ObjectStorage
|
12
|
+
include KnifeCloudformation::Utils::PathSelector
|
8
13
|
|
9
|
-
banner 'knife cloudformation import NEW_STACK_NAME JSON_EXPORT_FILE'
|
14
|
+
banner 'knife cloudformation import NEW_STACK_NAME [JSON_EXPORT_FILE]'
|
10
15
|
|
11
|
-
|
16
|
+
option(:path,
|
17
|
+
:long => '--import-path PATH',
|
18
|
+
:description => 'Directory path JSON export files are located',
|
19
|
+
:proc => lambda{|v|
|
20
|
+
Chef::Config[:knife][:cloudformation][:import][:path] = File.expand_path(v)
|
21
|
+
}
|
22
|
+
)
|
23
|
+
|
24
|
+
option(:bucket,
|
25
|
+
:long => '--export-bucket BUCKET_NAME',
|
26
|
+
:description => 'Remote file bucket JSON export files are located',
|
27
|
+
:proc => lambda{|v| Chef::Config[:knife][:cloudformation][:import][:bucket] = v}
|
28
|
+
)
|
29
|
+
|
30
|
+
option(:bucket_prefix,
|
31
|
+
:long => '--bucket-key-prefix PREFIX',
|
32
|
+
:description => 'Key prefix for file storage in bucket. Can be callable block if defined within configuration',
|
33
|
+
:proc => lambda{|v| Chef::Config[:knife][:cloudformation][:import][:bucket_prefix] = v}
|
34
|
+
)
|
35
|
+
|
36
|
+
unless(Chef::Config[:knife][:cloudformation].has_key?(:import))
|
37
|
+
Chef::Config[:knife][:cloudformation][:import] = Mash.new
|
38
|
+
end
|
39
|
+
|
40
|
+
# Run the import action
|
41
|
+
def _run
|
12
42
|
stack_name, json_file = name_args
|
13
|
-
ui.info "#{ui.color('Stack Import', :bold)}
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
43
|
+
ui.info "#{ui.color('Stack Import:', :bold)} #{stack_name}"
|
44
|
+
unless(json_file)
|
45
|
+
entries = [].tap do |_entries|
|
46
|
+
_entries.push('s3') if Chef::Config[:knife][:cloudformation][:import][:bucket]
|
47
|
+
_entries.push('fs') if Chef::Config[:knife][:cloudformation][:import][:path]
|
48
|
+
end
|
49
|
+
if(entries.size > 1)
|
50
|
+
valid = false
|
51
|
+
until(valid)
|
52
|
+
answer = ui.ask_question('Import via file system (fs) or remote bucket (remote)?', :default => 'remote')
|
53
|
+
valid = true if %w(remote fs).include?(answer)
|
54
|
+
entries = [answer]
|
55
|
+
end
|
56
|
+
elsif(entries.size < 1)
|
57
|
+
ui.fatal 'No path or bucket set. Unable to perform dynamic lookup!'
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
case entries.first
|
61
|
+
when 'remote'
|
62
|
+
json_file = remote_discovery
|
63
|
+
else
|
64
|
+
json_file = local_discovery
|
65
|
+
end
|
66
|
+
end
|
67
|
+
if(File.exists?(json_file) || json_file.is_a?(IO))
|
68
|
+
content = json_file.is_a?(IO) ? json_file.read : File.read(json_file)
|
69
|
+
export = Mash.new(_from_json(content))
|
70
|
+
begin
|
71
|
+
creator = Chef::Knife::CloudformationCreate.new
|
72
|
+
creator.name_args = [stack_name]
|
73
|
+
Chef::Config[:knife][:cloudformation][:template] = _from_json(export[:stack][:template])
|
74
|
+
Chef::Config[:knife][:cloudformation][:options] = export[:stack][:options]
|
75
|
+
ui.info ' - Starting creation of import'
|
76
|
+
creator.run
|
77
|
+
ui.info "#{ui.color('Stack Import', :bold)} (#{json_file}): #{ui.color('complete', :green)}"
|
78
|
+
rescue => e
|
79
|
+
ui.fatal "Failed to import stack: #{e}"
|
80
|
+
debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
|
81
|
+
exit -1
|
23
82
|
end
|
24
|
-
ui.info ' - Starting creation of import'
|
25
|
-
creator.run
|
26
|
-
ui.info "#{ui.color('Stack Import', :bold)} (#{json_file}): #{ui.color('complete', :green)}"
|
27
83
|
else
|
28
|
-
ui.
|
84
|
+
ui.fatal "Failed to locate JSON export file (#{json_file})"
|
29
85
|
exit 1
|
30
86
|
end
|
31
87
|
end
|
32
88
|
|
89
|
+
# Generate bucket prefix
|
90
|
+
#
|
91
|
+
# @return [String, NilClass]
|
92
|
+
def bucket_prefix
|
93
|
+
if(prefix = Chef::Config[:knife][:cloudformation][:import][:bucket_prefix])
|
94
|
+
if(prefix.respond_to?(:cal))
|
95
|
+
prefix.call
|
96
|
+
else
|
97
|
+
prefix.to_s
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Discover remote file
|
103
|
+
#
|
104
|
+
# @return [IO] stack export IO
|
105
|
+
def remote_discovery
|
106
|
+
storage = provider.service_for(:storage)
|
107
|
+
directory = storage.directories.get(
|
108
|
+
Chef::Config[:knife][:cloudformation][:import][:bucket]
|
109
|
+
)
|
110
|
+
file = prompt_for_file(
|
111
|
+
directory,
|
112
|
+
:directories_name => 'Collections',
|
113
|
+
:files_names => 'Exports',
|
114
|
+
:filter_prefix => bucket_prefix
|
115
|
+
)
|
116
|
+
if(file)
|
117
|
+
remote_file = storage.files.get(file)
|
118
|
+
StringIO.new(remote_file.body)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Discover remote file
|
123
|
+
#
|
124
|
+
# @return [IO] stack export IO
|
125
|
+
def local_discovery
|
126
|
+
_, bucket = Chef::Config[:knife][:cloudformation][:import][:path].split('/', 2)
|
127
|
+
storage = provider.service_for(:storage,
|
128
|
+
:provider => :local,
|
129
|
+
:local_root => '/'
|
130
|
+
)
|
131
|
+
directory = storage.directories.get(bucket)
|
132
|
+
prompt_for_file(
|
133
|
+
directory,
|
134
|
+
:directories_name => 'Collections',
|
135
|
+
:files_names => 'Exports'
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
33
139
|
end
|
34
140
|
end
|
35
141
|
end
|
@@ -1,111 +1,133 @@
|
|
1
|
-
require 'knife-cloudformation
|
2
|
-
require 'knife-cloudformation/utils'
|
1
|
+
require 'knife-cloudformation'
|
3
2
|
|
4
3
|
class Chef
|
5
4
|
class Knife
|
5
|
+
# Cloudformation inspect command
|
6
6
|
class CloudformationInspect < Knife
|
7
7
|
|
8
|
-
include KnifeCloudformation::
|
8
|
+
include KnifeCloudformation::Knife::Base
|
9
9
|
include KnifeCloudformation::Utils::Ssher
|
10
10
|
|
11
11
|
banner 'knife cloudformation inspect NAME'
|
12
12
|
|
13
|
-
option(:
|
14
|
-
:short => '-
|
15
|
-
:long => '--
|
13
|
+
option(:attribute,
|
14
|
+
:short => '-a ATTR',
|
15
|
+
:long => '--attribute ATTR',
|
16
|
+
:description => 'Dot delimited attribute to view'
|
17
|
+
)
|
18
|
+
|
19
|
+
option(:nodes,
|
20
|
+
:short => '-N',
|
21
|
+
:long => '--[no-]nodes',
|
16
22
|
:boolean => true,
|
17
|
-
:description => '
|
23
|
+
:description => 'Locate all instances'
|
24
|
+
)
|
25
|
+
|
26
|
+
option(:instance_failure,
|
27
|
+
:short => '-I [LOG_FILE_PATH]',
|
28
|
+
:long => '--instance-failure [LOG_FILE_PATH]',
|
29
|
+
:descrption => 'Display chef log file (defaults /var/log/chef/client.log)',
|
30
|
+
:proc => lambda{|val|
|
31
|
+
Chef::Config[:knife][:cloudformation][:failure_log_path] = val
|
32
|
+
}
|
18
33
|
)
|
19
34
|
|
20
35
|
option(:identity_file,
|
21
36
|
:short => '-i IDENTITY_FILE',
|
22
37
|
:long => '--identity-file IDENTITY_FILE',
|
23
|
-
:description => '
|
24
|
-
:proc => lambda
|
38
|
+
:description => 'SSH identity file for authentication',
|
39
|
+
:proc => lambda{|val|
|
25
40
|
Chef::Config[:knife][:cloudformation][:identity_file] = val
|
26
41
|
}
|
27
42
|
)
|
28
43
|
|
29
|
-
option(:nodes,
|
30
|
-
:short => '-N',
|
31
|
-
:long => '--nodes',
|
32
|
-
:boolean => true,
|
33
|
-
:description => 'Display ec2 nodes of stack'
|
34
|
-
)
|
35
|
-
|
36
44
|
option(:ssh_user,
|
37
45
|
:short => '-x SSH_USER',
|
38
46
|
:long => '--ssh-user SSH_USER',
|
39
|
-
:description => '
|
40
|
-
:proc => lambda
|
47
|
+
:description => 'SSH username for inspection connect',
|
48
|
+
:proc => lambda{|val|
|
41
49
|
Chef::Config[:knife][:cloudformation][:ssh_user] = val
|
42
50
|
}
|
43
51
|
)
|
44
52
|
|
45
|
-
|
53
|
+
# Run the stack inspection action
|
54
|
+
def _run
|
46
55
|
stack_name = name_args.last
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
def do_instance_failure(stack_name)
|
64
|
-
event = stack(stack_name).events.detect do |e|
|
65
|
-
e['ResourceType'] == 'AWS::CloudFormation::WaitCondition' &&
|
66
|
-
e['ResourceStatus'] == 'CREATE_FAILED' &&
|
67
|
-
e['ResourceStatusReason'].include?('uniqueId')
|
68
|
-
end
|
69
|
-
if(event)
|
70
|
-
process_instance_failure(stack_name, event)
|
71
|
-
else
|
72
|
-
ui.error "Failed to discover failed node within stack: #{stack_name}"
|
73
|
-
exit 1
|
56
|
+
stack = provider.stacks.get(stack_name)
|
57
|
+
ui.info "Stack inspection #{ui.color(stack_name, :bold)}:"
|
58
|
+
outputs = [:attribute, :nodes, :instance_failure].map do |key|
|
59
|
+
if(config.has_key?(key))
|
60
|
+
send("display_#{key}", stack)
|
61
|
+
key
|
62
|
+
end
|
63
|
+
end.compact
|
64
|
+
if(outputs.empty?)
|
65
|
+
ui.info ' Stack dump:'
|
66
|
+
ui.info MultiJson.dump(
|
67
|
+
MultiJson.load(
|
68
|
+
stack.reload.to_json
|
69
|
+
),
|
70
|
+
:pretty => true
|
71
|
+
)
|
74
72
|
end
|
75
73
|
end
|
76
74
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
75
|
+
def display_instance_failure(stack)
|
76
|
+
instances = stack.resources.all.find_all do |resource|
|
77
|
+
resource.state.to_s.end_with?('failed')
|
78
|
+
end.map do |resource|
|
79
|
+
# If compute instance, simply expand
|
80
|
+
if(resource.within?(:compute, :servers))
|
81
|
+
resource.instance
|
82
|
+
# If a waitcondition, check for instance ID
|
83
|
+
elsif(resource.type.to_s.downcase.end_with?('waitcondition'))
|
84
|
+
if(resource.status_reason.to_s.include?('uniqueId'))
|
85
|
+
srv_id = resource.status_reason.split(' ').last.strip
|
86
|
+
provider.connection.api_for(:compute).servers.get(srv_id)
|
87
|
+
end
|
90
88
|
end
|
91
|
-
end
|
92
|
-
if(
|
93
|
-
ui.
|
94
|
-
ui.info ""
|
95
|
-
ui.info content
|
89
|
+
end.compact
|
90
|
+
if(instances.empty?)
|
91
|
+
ui.error 'Failed to locate any failed instances'
|
96
92
|
else
|
97
|
-
|
93
|
+
log_path = Chef::Config[:knife][:cloudformation][:failure_log_path]
|
94
|
+
if(log_path.to_s.empty?)
|
95
|
+
log_path = '/var/log/chef/client.log'
|
96
|
+
end
|
97
|
+
opts = ssh_key ? {:keys => [ssh_key]} : {}
|
98
|
+
instances.each do |instance|
|
99
|
+
ui.info " -> Log inspect for #{instance.id}:"
|
100
|
+
address = instance.addresses_public.map do |address|
|
101
|
+
if(address.version == 4)
|
102
|
+
address.address
|
103
|
+
end
|
104
|
+
end
|
105
|
+
if(address)
|
106
|
+
ssh_attempt_users.each do |user|
|
107
|
+
begin
|
108
|
+
ui.info remote_file_contents(address.first, user, log_path, opts)
|
109
|
+
break
|
110
|
+
rescue Net::SSH::AuthenticationFailed
|
111
|
+
ui.warn "Authentication failed for user #{user} on instance #{address}"
|
112
|
+
rescue => e
|
113
|
+
ui.error "Failed to retrieve log: #{e}"
|
114
|
+
_debug e
|
115
|
+
break
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
98
120
|
end
|
99
121
|
end
|
100
122
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
Chef::Config[:knife][:cloudformation][:ssh_user] ||
|
123
|
+
# Users to attempt SSH connection
|
124
|
+
#
|
125
|
+
# @return [Array<String>] usernames for ssh connect attempt
|
126
|
+
def ssh_attempt_users
|
127
|
+
base_user = Chef::Config[:knife][:cloudformation][:ssh_user] ||
|
107
128
|
Chef::Config[:knife][:ssh_user] ||
|
108
129
|
ENV['USER']
|
130
|
+
[base_user, Chef::Config[:knife][:cloudformation][:ssh_attempt_users]].flatten.compact
|
109
131
|
end
|
110
132
|
|
111
133
|
def ssh_key
|
@@ -113,6 +135,72 @@ class Chef
|
|
113
135
|
Chef::Config[:knife][:identity_file]
|
114
136
|
end
|
115
137
|
|
138
|
+
def display_attribute(stack)
|
139
|
+
attr = config[:attribute].split('.').inject(stack) do |memo, key|
|
140
|
+
args = key.scan(/\(([^)]*)\)/).flatten.first.to_s
|
141
|
+
if(args)
|
142
|
+
args = args.split(',').map{|a| a.to_i.to_s == a ? a.to_i : a}
|
143
|
+
key = key.split('(').first
|
144
|
+
end
|
145
|
+
if(memo.public_methods.include?(key.to_sym))
|
146
|
+
if(args.size == 1 && args.first.to_s.start_with?('&'))
|
147
|
+
memo.send(key, &args.first.slice(2, args.first.size).to_sym)
|
148
|
+
else
|
149
|
+
memo.send(*[key, args].flatten.compact)
|
150
|
+
end
|
151
|
+
else
|
152
|
+
raise NoMethodError.new "Invalid attribute requested! (#{memo.class}##{key})"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
ui.info " Attribute Lookup -> #{config[:attribute]}:"
|
156
|
+
ui.info MultiJson.dump(
|
157
|
+
MultiJson.load(
|
158
|
+
MultiJson.dump(attr)
|
159
|
+
),
|
160
|
+
:pretty => true
|
161
|
+
)
|
162
|
+
end
|
163
|
+
|
164
|
+
def display_nodes(stack)
|
165
|
+
asg_nodes = Smash[
|
166
|
+
stack.resources.all.find_all do |resource|
|
167
|
+
resource.within?(:auto_scale, :groups)
|
168
|
+
end.map do |group_resource|
|
169
|
+
asg = group_resource.expand
|
170
|
+
[
|
171
|
+
asg.name,
|
172
|
+
Smash[
|
173
|
+
asg.servers.map(&:expand).map{|s|
|
174
|
+
[s.id, Smash.new(
|
175
|
+
:name => s.name,
|
176
|
+
:addresses => s.addresses.map(&:address)
|
177
|
+
)]
|
178
|
+
}
|
179
|
+
]
|
180
|
+
]
|
181
|
+
end
|
182
|
+
]
|
183
|
+
compute_nodes = Smash[
|
184
|
+
stack.resources.all.find_all do |resource|
|
185
|
+
resource.within?(:compute, :servers)
|
186
|
+
end.map do |srv|
|
187
|
+
srv = srv.instance
|
188
|
+
[srv.id, Smash.new(
|
189
|
+
:name => srv.name,
|
190
|
+
:addresses => srv.addresses.map(&:address)
|
191
|
+
)]
|
192
|
+
end
|
193
|
+
]
|
194
|
+
unless(asg_nodes.empty?)
|
195
|
+
ui.info ' AutoScale Group Instances:'
|
196
|
+
ui.info MultiJson.dump(asg_nodes, :pretty => true)
|
197
|
+
end
|
198
|
+
unless(compute_nodes.empty?)
|
199
|
+
ui.info ' Compute Instances:'
|
200
|
+
ui.info MultiJson.dump(compute_nodes, :pretty => true)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
116
204
|
end
|
117
205
|
end
|
118
206
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
-
require 'knife-cloudformation
|
1
|
+
require 'knife-cloudformation'
|
2
2
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
5
|
+
# Cloudformation list command
|
5
6
|
class CloudformationList < Knife
|
6
|
-
|
7
|
+
|
8
|
+
include KnifeCloudformation::Knife::Base
|
7
9
|
|
8
10
|
banner 'knife cloudformation list NAME'
|
9
11
|
|
@@ -32,43 +34,37 @@ class Chef
|
|
32
34
|
}
|
33
35
|
)
|
34
36
|
|
35
|
-
|
37
|
+
# Run the list command
|
38
|
+
def _run
|
36
39
|
things_output(nil, get_list, nil)
|
37
40
|
end
|
38
41
|
|
42
|
+
# Get the list of stacks to display
|
43
|
+
#
|
44
|
+
# @return [Array<Hash>]
|
39
45
|
def get_list
|
40
46
|
get_things do
|
41
|
-
|
42
|
-
|
47
|
+
provider.stacks.all.map do |stack|
|
48
|
+
Mash.new(stack.attributes)
|
49
|
+
end.sort do |x, y|
|
50
|
+
if(y[:created].to_s.empty?)
|
43
51
|
-1
|
44
|
-
elsif(x[
|
52
|
+
elsif(x[:created].to_s.empty?)
|
45
53
|
1
|
46
54
|
else
|
47
|
-
Time.parse(y['
|
55
|
+
Time.parse(y['created'].to_s) <=> Time.parse(x['created'].to_s)
|
48
56
|
end
|
49
57
|
end
|
50
58
|
end
|
51
59
|
end
|
52
60
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
filter = {}
|
61
|
+
# @return [Array<String>] default attributes to display
|
62
|
+
def default_attributes
|
63
|
+
if(provider.connection.provider == :aws)
|
64
|
+
%w(name created status template_description)
|
58
65
|
else
|
59
|
-
|
60
|
-
filter = Hash[*(
|
61
|
-
status.map do |n|
|
62
|
-
count += 1
|
63
|
-
["StackStatusFilter.member.#{count}", n]
|
64
|
-
end.flatten
|
65
|
-
)]
|
66
|
+
%w(name created status description)
|
66
67
|
end
|
67
|
-
filter
|
68
|
-
end
|
69
|
-
|
70
|
-
def default_attributes
|
71
|
-
%w(StackName CreationTime StackStatus TemplateDescription)
|
72
68
|
end
|
73
69
|
|
74
70
|
end
|