knife-cloudformation 0.1.22 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|