cfn_manage 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/cfn_manage +2 -0
- data/bin/cfn_manage.rb +95 -0
- data/bin/usage.txt +42 -0
- data/lib/aws_credentials.rb +36 -0
- data/lib/cf_common.rb +27 -0
- data/lib/cf_progress_tracker.rb +55 -0
- data/lib/cf_start_stop_environment.rb +357 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6e4f78a5035f0689fbfc63085368bd1614fe61bc
|
4
|
+
data.tar.gz: 155ff3f1c89b73db2412761cc95e6924a7c6be53
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b64b4a25627848983eea551310e6c7dc5fb6075dc66e3e41731683b78f86bc60909ef15d21662115df876864e63bfa812b57688a734f93ccd7e3e6d42bf052f1
|
7
|
+
data.tar.gz: 5e403600572982b4c5d92e48cba19329ce500655727cfe521770cd3a6204e7ce24579cce71d996803a66fb014ca2686e9728d5283e65ff3a888222909ccc34f7
|
data/bin/cfn_manage
ADDED
data/bin/cfn_manage.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require_relative '../lib/cf_common'
|
3
|
+
require_relative '../lib/cf_start_stop_environment'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
# exit with usage information
|
7
|
+
def print_usage_exit(code)
|
8
|
+
STDERR.puts(File.open("#{File.expand_path(File.dirname(__FILE__))}/usage.txt").read)
|
9
|
+
exit code
|
10
|
+
end
|
11
|
+
|
12
|
+
# global options
|
13
|
+
$options = {}
|
14
|
+
$options['SOURCE_BUCKET'] = ENV['SOURCE_BUCKET']
|
15
|
+
$options['AWS_ASSUME_ROLE'] = ENV['AWS_ASSUME_ROLE']
|
16
|
+
|
17
|
+
# global logger
|
18
|
+
$log = Logger.new(STDOUT)
|
19
|
+
|
20
|
+
# always flush output
|
21
|
+
STDOUT.sync = true
|
22
|
+
|
23
|
+
# parse command line options
|
24
|
+
OptionParser.new do |opts|
|
25
|
+
|
26
|
+
opts.banner = 'Usage: cfn_manage [command] [options]'
|
27
|
+
|
28
|
+
opts.on('--source-bucket [BUCKET]') do |bucket|
|
29
|
+
$options['SOURCE_BUCKET'] = bucket
|
30
|
+
ENV['SOURCE_BUCKET'] = bucket
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on('--aws-role [ROLE]') do |role|
|
34
|
+
ENV['AWS_ASSUME_ROLE'] = role
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on('--stack-name [STACK_NAME]') do |stack|
|
38
|
+
$options['STACK'] = stack
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on('--asg-name [ASG]') do |asg|
|
42
|
+
$options['ASG'] = asg
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on('--rds-instance-id [RDS_INSTANCE_ID]') do |asg|
|
46
|
+
$options['RDS_INSTANCE_ID'] = asg
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on('--stack-name [STACK_NAME]') do |asg|
|
50
|
+
$options['STACK_NAME'] = asg
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on('-r [AWS_REGION]', '--region [AWS_REGION]') do |region|
|
54
|
+
ENV['AWS_REGION'] = region
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.on('-p [AWS_PROFILE]', '--profile [AWS_PROFILE]') do |profile|
|
58
|
+
ENV['CFN_AWS_PROFILE'] = profile
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.on('--dry-run') do
|
62
|
+
ENV['DRY_RUN'] = '1'
|
63
|
+
end
|
64
|
+
|
65
|
+
end.parse!
|
66
|
+
|
67
|
+
command = ARGV[0]
|
68
|
+
|
69
|
+
if command.nil?
|
70
|
+
print_usage_exit(-1)
|
71
|
+
end
|
72
|
+
|
73
|
+
# execute action based on command
|
74
|
+
case command
|
75
|
+
when 'help'
|
76
|
+
print_usage_exit(0)
|
77
|
+
# asg commands
|
78
|
+
when 'stop-asg'
|
79
|
+
Base2::CloudFormation::EnvironmentRunStop.new().start_stop_asg('stop', $options['ASG'])
|
80
|
+
when 'start-asg'
|
81
|
+
Base2::CloudFormation::EnvironmentRunStop.new().start_stop_asg('start', $options['ASG'])
|
82
|
+
|
83
|
+
# rds commands
|
84
|
+
when 'stop-rds'
|
85
|
+
Base2::CloudFormation::EnvironmentRunStop.new().start_stop_rds('stop', $options['RDS_INSTANCE_ID'])
|
86
|
+
when 'start-rds'
|
87
|
+
Base2::CloudFormation::EnvironmentRunStop.new().start_stop_rds('start', $options['RDS_INSTANCE_ID'])
|
88
|
+
|
89
|
+
# stack commands
|
90
|
+
# rds commands
|
91
|
+
when 'stop-environment'
|
92
|
+
Base2::CloudFormation::EnvironmentRunStop.new().stop_environment($options['STACK_NAME'])
|
93
|
+
when 'start-environment'
|
94
|
+
Base2::CloudFormation::EnvironmentRunStop.new().start_environment($options['STACK_NAME'])
|
95
|
+
end
|
data/bin/usage.txt
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Usage: cfn_manage [command] [options]
|
2
|
+
|
3
|
+
Commands:
|
4
|
+
|
5
|
+
cfn_manage stop-environment --stack-name [STACK_NAME]
|
6
|
+
|
7
|
+
cfn_manage start-environment --stack-name [STACK_NAME]
|
8
|
+
|
9
|
+
cfn_manage stop-asg --asg-name [ASG]
|
10
|
+
|
11
|
+
cfn_manage start-asg --asg-name [ASG]
|
12
|
+
|
13
|
+
cfn_manage stop-rds --rds-instance-id [RDS_INSTANCE_ID]
|
14
|
+
|
15
|
+
cfn_manage start-rds --rds-instance-id [RDS_INSTANCE_ID]
|
16
|
+
|
17
|
+
|
18
|
+
General options
|
19
|
+
|
20
|
+
--source-bucket [BUCKET]
|
21
|
+
|
22
|
+
Pucket used to store / pull information from
|
23
|
+
|
24
|
+
--aws-role [ROLE_ARN]
|
25
|
+
|
26
|
+
AWS Role to assume when performing operations. Any reads and
|
27
|
+
write to source bucket will be performed outside of this role
|
28
|
+
|
29
|
+
|
30
|
+
-r [AWS_REGION], --region [AWS_REGION]
|
31
|
+
|
32
|
+
AWS Region to use when making API calls
|
33
|
+
|
34
|
+
-p [AWS_PROFILE], --profile [AWS_PROFILE]
|
35
|
+
|
36
|
+
AWS Shared profile to use when making API calls
|
37
|
+
|
38
|
+
--dry-run
|
39
|
+
|
40
|
+
Applicable only to [start|stop-environment] commands. If dry run is enabled
|
41
|
+
info about assets being started / stopped will ne only printed to standard output,
|
42
|
+
without any action taken.
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module Base2
|
4
|
+
|
5
|
+
class AWSCredentials
|
6
|
+
|
7
|
+
def self.get_session_credentials(session_name)
|
8
|
+
|
9
|
+
#check if AWS_ASSUME_ROLE exists
|
10
|
+
session_name = "#{session_name.gsub('_','-')}-#{Time.now.getutc.to_i}"
|
11
|
+
if session_name.length > 64
|
12
|
+
session_name = session_name[-64..-1]
|
13
|
+
end
|
14
|
+
assume_role = ENV['AWS_ASSUME_ROLE'] or nil
|
15
|
+
if not assume_role.nil?
|
16
|
+
return Aws::AssumeRoleCredentials.new(
|
17
|
+
role_arn: assume_role,
|
18
|
+
role_session_name: session_name
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# check if explicitly set shared credentials profile
|
23
|
+
if ENV.key?('CFN_AWS_PROFILE')
|
24
|
+
return Aws::SharedCredentials.new(profile_name: ENV['CFN_AWS_PROFILE'])
|
25
|
+
end
|
26
|
+
|
27
|
+
# check if Instance Profile available
|
28
|
+
credentials = Aws::InstanceProfileCredentials.new(retries: 2, http_open_timeout:1)
|
29
|
+
return credentials unless credentials.credentials.access_key_id.nil?
|
30
|
+
|
31
|
+
# use default profile
|
32
|
+
return Aws::SharedCredentials.new()
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/cf_common.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Base2
|
2
|
+
|
3
|
+
module CloudFormation
|
4
|
+
|
5
|
+
class Common
|
6
|
+
|
7
|
+
def self.visit_stack(cf_client, stack_name, handler, visit_substacks)
|
8
|
+
stack_resources = cf_client.describe_stack_resources(stack_name: stack_name)
|
9
|
+
stack = cf_client.describe_stacks(stack_name: stack_name)
|
10
|
+
|
11
|
+
# call traverse handler for parent stack
|
12
|
+
handler.call(stack['stacks'][0].stack_name)
|
13
|
+
|
14
|
+
# do not traverse unless instructed
|
15
|
+
return unless visit_substacks
|
16
|
+
|
17
|
+
stack_resources['stack_resources'].each do |resource|
|
18
|
+
# test if resource us substack
|
19
|
+
unless (resource['physical_resource_id'] =~ /arn:aws:cloudformation:(.*):stack\/(.*)/).nil?
|
20
|
+
# call recursively
|
21
|
+
self.visit_stack(cf_client, resource['physical_resource_id'], handler, visit_substacks)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require_relative './cf_common'
|
3
|
+
|
4
|
+
module Base2
|
5
|
+
module CloudFormation
|
6
|
+
class ProgressTracker
|
7
|
+
@cf_client = nil
|
8
|
+
@stack_name = nil
|
9
|
+
@last_event_times = {}
|
10
|
+
@period_from = nil
|
11
|
+
|
12
|
+
@@default_ending_states = %w[
|
13
|
+
CREATE_COMPLETE
|
14
|
+
UPDATE_COMPLETE
|
15
|
+
UPDATE_ROLLBACK_COMPLETE
|
16
|
+
ROLLBACK_FAILED
|
17
|
+
DELETE_FAILED
|
18
|
+
]
|
19
|
+
|
20
|
+
@@default_display_state = %w[
|
21
|
+
CREATE_COMPLETE
|
22
|
+
UPDATE_COMPLETE
|
23
|
+
]
|
24
|
+
|
25
|
+
@ending_states = nil
|
26
|
+
|
27
|
+
def initialize(stack_name, period_from, creds = nil, region = nil)
|
28
|
+
client_params = {}
|
29
|
+
client_params['region'] = region unless region.nil?
|
30
|
+
client_params['credentials'] = creds unless creds.nil?
|
31
|
+
@cf_client = Aws::CloudFormation::Client.new(client_params)
|
32
|
+
@stack_name = stack_name
|
33
|
+
@ending_statest = @@default_ending_states
|
34
|
+
@period_from = period_from
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def track_single_stack(stack)
|
39
|
+
stack_id = stack['stack_id']
|
40
|
+
# Default to period_from if first run, take from last run otherwise
|
41
|
+
event_from = last_event_times[stack_id] if @last_event_times.key?(stack_id)
|
42
|
+
event_from = @period_from unless @last_event_times.key?(stack_id)
|
43
|
+
|
44
|
+
|
45
|
+
stack_resources = @cf_client.describe_stack_events(stack_name: stack['stack_id'],)
|
46
|
+
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def track_progress(_show_only_failures = false)
|
51
|
+
Common.visit_stack(@cf_client, @stack_name, method(:track_single_stack),true)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,357 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require_relative '../lib/cf_common'
|
3
|
+
require_relative '../lib/aws_credentials'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module Base2
|
8
|
+
module CloudFormation
|
9
|
+
class EnvironmentRunStop
|
10
|
+
|
11
|
+
@cf_client = nil
|
12
|
+
@stack_name = nil
|
13
|
+
@s3_client = nil
|
14
|
+
@s3_bucket = nil
|
15
|
+
@credentials = nil
|
16
|
+
@dry_run = false
|
17
|
+
@@supported_start_stop_resources = {
|
18
|
+
'AWS::AutoScaling::AutoScalingGroup' => 'start_stop_asg',
|
19
|
+
'AWS::RDS::DBInstance' => 'start_stop_rds',
|
20
|
+
'AWS::EC2::Instance' => 'start_stop_ec2'
|
21
|
+
}
|
22
|
+
|
23
|
+
@@resource_start_priorities = {
|
24
|
+
'AWS::RDS::DBInstance' => '100',
|
25
|
+
'AWS::AutoScaling::AutoScalingGroup' => '200',
|
26
|
+
'AWS::EC2::Instance' => '200'
|
27
|
+
}
|
28
|
+
|
29
|
+
@environment_resources = nil
|
30
|
+
|
31
|
+
def initialize()
|
32
|
+
@environment_resources = []
|
33
|
+
@s3_client = Aws::S3::Client.new()
|
34
|
+
@s3_bucket = ENV['SOURCE_BUCKET']
|
35
|
+
@cf_client = Aws::CloudFormation::Client.new()
|
36
|
+
@credentials = Base2::AWSCredentials.get_session_credentials('start_stop_environment')
|
37
|
+
if not @credentials.nil?
|
38
|
+
@cf_client = Aws::CloudFormation::Client.new(credentials: @credentials)
|
39
|
+
end
|
40
|
+
@dry_run = ENV.key?('DRY_RUN') and ENV['DRY_RUN'] == '1'
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def start_environment(stack_name)
|
45
|
+
$log.info("Starting environment #{stack_name}")
|
46
|
+
Common.visit_stack(@cf_client, stack_name, method(:collect_resources), true)
|
47
|
+
do_start_assets
|
48
|
+
configuration = {stack_running: true}
|
49
|
+
save_item_configuration("environment-data/stack-data/#{stack_name}", configuration) unless @dry_run
|
50
|
+
$log.info("Environment #{stack_name} started")
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def stop_environment(stack_name)
|
55
|
+
$log.info("Stopping environment #{stack_name}")
|
56
|
+
Common.visit_stack(@cf_client, stack_name, method(:collect_resources), true)
|
57
|
+
do_stop_assets
|
58
|
+
configuration = {stack_running: false}
|
59
|
+
save_item_configuration("environment-data/stack-data/#{stack_name}", configuration) unless @dry_run
|
60
|
+
$log.info("Environment #{stack_name} stopped")
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def do_stop_assets
|
65
|
+
# sort start resource by priority
|
66
|
+
@environment_resources = @environment_resources.sort_by { |k| k[:priority]}.reverse
|
67
|
+
|
68
|
+
@environment_resources.each do |resource|
|
69
|
+
$log.info("Stopping resource #{resource[:id]}")
|
70
|
+
# just print out information if running a dry run, otherwise start assets
|
71
|
+
if not @dry_run
|
72
|
+
eval "self.#{resource[:method]}('stop','#{resource[:id]}')"
|
73
|
+
else
|
74
|
+
$log.info("Dry run enabled, skipping stop start\nFollowing resource would be stopped: #{resource[:id]}")
|
75
|
+
$log.debug("Resource type: #{resource[:type]}\n\n")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def do_start_assets
|
82
|
+
# sort start resource by priority
|
83
|
+
@environment_resources = @environment_resources.sort_by { |k| k[:priority]}
|
84
|
+
|
85
|
+
@environment_resources.each do |resource|
|
86
|
+
$log.info("Starting resource #{resource[:id]}")
|
87
|
+
# just print out information if running a dry run, otherwise start assets
|
88
|
+
if not @dry_run
|
89
|
+
eval "self.#{resource[:method]}('start','#{resource[:id]}')"
|
90
|
+
else
|
91
|
+
$log.info("Dry run enabled, skipping actual start\nFollowing resource would be started: #{resource[:id]}")
|
92
|
+
$log.debug("Resource type: #{resource[:type]}\n\n")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def collect_resources(stack_name)
|
98
|
+
resrouces = @cf_client.describe_stack_resources(stack_name: stack_name)
|
99
|
+
resrouces['stack_resources'].each do |resource|
|
100
|
+
if @@supported_start_stop_resources.key?(resource['resource_type'])
|
101
|
+
method_name = @@supported_start_stop_resources[resource['resource_type']]
|
102
|
+
resource_id = resource['physical_resource_id']
|
103
|
+
|
104
|
+
@environment_resources << {
|
105
|
+
id: resource_id,
|
106
|
+
priority: @@resource_start_priorities[resource['resource_type']],
|
107
|
+
method: method_name,
|
108
|
+
type: resource['resource_type']
|
109
|
+
}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def start_stop_ec2(cmd, instance_id)
|
115
|
+
credentials = Base2::AWSCredentials.get_session_credentials("stoprun_#{instance_id}")
|
116
|
+
ec2_client = Aws::EC2::Client.new(credentials: credentials)
|
117
|
+
|
118
|
+
begin
|
119
|
+
instance = Aws::EC2::Resource.new(client: ec2_client).instance(instance_id)
|
120
|
+
|
121
|
+
if cmd == 'stop'
|
122
|
+
if %w(stopped stopping).include?(instance.state.name)
|
123
|
+
$log.info("Instance #{instance_id} already stopping or stopped")
|
124
|
+
return
|
125
|
+
end
|
126
|
+
$log.info("Stopping instance #{instance_id}")
|
127
|
+
instance.stop()
|
128
|
+
end
|
129
|
+
|
130
|
+
if cmd == 'start'
|
131
|
+
if %w(running).include?(instance.state.name)
|
132
|
+
$log.info("Instance #{instance_id} already running")
|
133
|
+
return
|
134
|
+
end
|
135
|
+
$log.info("Starting instance #{instance_id}")
|
136
|
+
instance.start()
|
137
|
+
end
|
138
|
+
rescue => e
|
139
|
+
$log.error("Failed execution #{cmd} on instance #{instance_id}:\n#{e}")
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
def start_stop_asg(cmd, asg_name)
|
145
|
+
|
146
|
+
# read asg data
|
147
|
+
credentials = Base2::AWSCredentials.get_session_credentials("stopasg_#{asg_name}")
|
148
|
+
asg_client = Aws::AutoScaling::Client.new()
|
149
|
+
if credentials != nil
|
150
|
+
asg_client = Aws::AutoScaling::Client.new(credentials: credentials)
|
151
|
+
end
|
152
|
+
|
153
|
+
asg_details = asg_client.describe_auto_scaling_groups(
|
154
|
+
auto_scaling_group_names: [asg_name]
|
155
|
+
)
|
156
|
+
if asg_details.auto_scaling_groups.size() == 0
|
157
|
+
raise "Couldn't find ASG #{asg_name}"
|
158
|
+
end
|
159
|
+
asg = asg_details.auto_scaling_groups[0]
|
160
|
+
s3_prefix = "environment-data/asg-data/#{asg_name}"
|
161
|
+
case cmd
|
162
|
+
when 'start'
|
163
|
+
|
164
|
+
# retrieve asg params from s3
|
165
|
+
configuration = self.get_object_configuration(s3_prefix)
|
166
|
+
if configuration.nil?
|
167
|
+
$log.warn("No configuration found for #{asg_name}, skipping..")
|
168
|
+
return
|
169
|
+
end
|
170
|
+
$log.info("Starting ASG #{asg_name} with following configuration\n#{configuration}")
|
171
|
+
|
172
|
+
# restore asg sizes
|
173
|
+
asg_client.update_auto_scaling_group({
|
174
|
+
auto_scaling_group_name: asg_name,
|
175
|
+
min_size: configuration['min_size'],
|
176
|
+
max_size: configuration['max_size'],
|
177
|
+
desired_capacity: configuration['desired_capacity']
|
178
|
+
})
|
179
|
+
|
180
|
+
when 'stop'
|
181
|
+
# check if already stopped
|
182
|
+
if asg.min_size == asg.max_size and asg.max_size == asg.desired_capacity and asg.min_size == 0
|
183
|
+
$log.info("ASG #{asg_name} already stopped")
|
184
|
+
else
|
185
|
+
# store asg configuration to S3
|
186
|
+
configuration = {
|
187
|
+
desired_capacity: asg.desired_capacity,
|
188
|
+
min_size: asg.min_size,
|
189
|
+
max_size: asg.max_size
|
190
|
+
}
|
191
|
+
self.save_item_configuration(s3_prefix, configuration)
|
192
|
+
|
193
|
+
$log.info("Setting desired capacity to 0/0/0 for ASG #{asg_name}")
|
194
|
+
# set asg configuration to 0/0/0
|
195
|
+
asg_client.update_auto_scaling_group({
|
196
|
+
auto_scaling_group_name: asg_name,
|
197
|
+
min_size: 0,
|
198
|
+
max_size: 0,
|
199
|
+
desired_capacity: 0
|
200
|
+
})
|
201
|
+
end
|
202
|
+
# TODO wait for operation to complete (optionally)
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
def start_stop_rds(cmd, instance_id)
|
209
|
+
credentials = Base2::AWSCredentials.get_session_credentials("startstoprds_#{instance_id}")
|
210
|
+
rds_client = Aws::RDS::Client.new()
|
211
|
+
if credentials != nil
|
212
|
+
rds_client = Aws::RDS::Client.new(credentials: credentials)
|
213
|
+
end
|
214
|
+
rds = Aws::RDS::Resource.new(client: rds_client)
|
215
|
+
rds_instance = rds.db_instance(instance_id)
|
216
|
+
s3_prefix = "environment-data/rds-data/#{instance_id}"
|
217
|
+
case cmd
|
218
|
+
when 'start'
|
219
|
+
if rds_instance.db_instance_status == 'available'
|
220
|
+
$log.info("RDS Instance #{instance_id} is already in available state")
|
221
|
+
return
|
222
|
+
end
|
223
|
+
|
224
|
+
#retrieve multi-az data from S3
|
225
|
+
configuration = get_object_configuration(s3_prefix)
|
226
|
+
if configuration.nil?
|
227
|
+
$log.warning("No configuration found for #{rds_instance}, skipping..")
|
228
|
+
return
|
229
|
+
end
|
230
|
+
|
231
|
+
# start rds instance
|
232
|
+
if rds_instance.db_instance_status == 'stopped'
|
233
|
+
$log.info("Starting db instance #{instance_id}")
|
234
|
+
rds_client.start_db_instance({ db_instance_identifier: instance_id })
|
235
|
+
|
236
|
+
# wait instance to become available
|
237
|
+
$log.info("Waiting db instance to become available #{instance_id}")
|
238
|
+
wait_rds_instance_states(rds_client, instance_id, %w(starting available))
|
239
|
+
else
|
240
|
+
wait_rds_instance_states(rds_client, instance_id, %w(available))
|
241
|
+
end
|
242
|
+
|
243
|
+
# convert rds instance to mutli-az if required
|
244
|
+
if configuration['is_multi_az']
|
245
|
+
$log.info("Converting to Multi-AZ instance after start (instance #{instance_id})")
|
246
|
+
set_rds_instance_multi_az(rds_instance, true)
|
247
|
+
end
|
248
|
+
|
249
|
+
when 'stop'
|
250
|
+
# store mutli-az data to S3
|
251
|
+
if rds_instance.db_instance_status != 'available'
|
252
|
+
$log.warn("RDS Instance #{instance_id} not in available state, and thus can not be stopped")
|
253
|
+
$log.warn("RDS Instance #{instance_id} state: #{rds_instance.db_instance_status}")
|
254
|
+
return
|
255
|
+
end
|
256
|
+
|
257
|
+
if rds_instance.db_instance_status == 'stopped'
|
258
|
+
$log.info("RDS Instance #{instance_id} is already stopped")
|
259
|
+
return
|
260
|
+
end
|
261
|
+
|
262
|
+
configuration = {
|
263
|
+
is_multi_az: rds_instance.multi_az
|
264
|
+
}
|
265
|
+
save_item_configuration(s3_prefix, configuration)
|
266
|
+
|
267
|
+
#check if mutli-az RDS. if so, convert to single-az
|
268
|
+
if rds_instance.multi_az
|
269
|
+
$log.info("Converting to Non-Multi-AZ instance before stop (instance #{instance_id}")
|
270
|
+
set_rds_instance_multi_az(rds_instance, false)
|
271
|
+
end
|
272
|
+
|
273
|
+
# stop rds instance and wait for it to be fully stopped
|
274
|
+
$log.info("Stopping instance #{instance_id}")
|
275
|
+
rds_client.stop_db_instance({ db_instance_identifier: instance_id })
|
276
|
+
$log.info("Waiting db instance to be stopped #{instance_id}")
|
277
|
+
wait_rds_instance_states(rds_client, instance_id, %w(stopping stopped))
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def set_rds_instance_multi_az(rds_instance, multi_az)
|
282
|
+
if rds_instance.multi_az == multi_az
|
283
|
+
$log.info("Rds instance #{rds_instance.db_instance_identifier} already multi-az=#{multi_az}")
|
284
|
+
return
|
285
|
+
end
|
286
|
+
rds_instance.modify({ multi_az: multi_az, apply_immediately: true })
|
287
|
+
# allow half an hour for instance to be converted
|
288
|
+
wait_states = %w(modifying available)
|
289
|
+
wait_rds_instance_states(rds_instance, wait_states)
|
290
|
+
end
|
291
|
+
|
292
|
+
def wait_rds_instance_states(client, rds_instance_id, wait_states)
|
293
|
+
wait_states.each do |state|
|
294
|
+
# reached state must be steady, at least a minute. Modifying an instance to/from MultiAZ can't be shorter
|
295
|
+
# than 40 seconds, hence steady count is 4
|
296
|
+
state_count = 0
|
297
|
+
steady_count = 4
|
298
|
+
attempts = 0
|
299
|
+
rds = Aws::RDS::Resource.new(client: client)
|
300
|
+
until attempts == (max_attempts = 60*6) do
|
301
|
+
instance = rds.db_instance(rds_instance_id)
|
302
|
+
$log.info("Instance #{instance.db_instance_identifier} state: #{instance.db_instance_status}, waiting for #{state}")
|
303
|
+
|
304
|
+
if instance.db_instance_status == "#{state}"
|
305
|
+
state_count = state_count + 1
|
306
|
+
$log.info("#{state_count}/#{steady_count}")
|
307
|
+
else
|
308
|
+
state_count = 0
|
309
|
+
end
|
310
|
+
break if state_count == steady_count
|
311
|
+
attempts = attempts + 1
|
312
|
+
sleep(15)
|
313
|
+
end
|
314
|
+
|
315
|
+
if attempts == max_attempts
|
316
|
+
$log.error("RDS Database Instance #{rds_instance_id} did not enter #{state} state, however continuing operations...")
|
317
|
+
end
|
318
|
+
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
|
323
|
+
def get_object_configuration(s3_prefix)
|
324
|
+
configuration = nil
|
325
|
+
begin
|
326
|
+
key = "#{s3_prefix}/latest/config.json"
|
327
|
+
$log.info("Reading object configuration from s3://#{@s3_bucket}/#{key}")
|
328
|
+
|
329
|
+
# fetch and deserialize and s3 object
|
330
|
+
configuration = JSON.parse(@s3_client.get_object(bucket: @s3_bucket, key: key).body.read)
|
331
|
+
|
332
|
+
$log.info("Configuration:#{configuration}")
|
333
|
+
rescue Aws::S3::Errors::NoSuchKey
|
334
|
+
$log.warn("Could not find configuration at s3://#{@s3_bucket}/#{key}")
|
335
|
+
end
|
336
|
+
configuration
|
337
|
+
end
|
338
|
+
|
339
|
+
def save_item_configuration(s3_prefix, configuration)
|
340
|
+
# save latest configuration, and one time-based versioned
|
341
|
+
s3_keys = [
|
342
|
+
"#{s3_prefix}/latest/config.json",
|
343
|
+
"#{s3_prefix}/#{Time.now.getutc.to_i}/config.json"
|
344
|
+
]
|
345
|
+
s3_keys.each do |key|
|
346
|
+
$log.info("Saving configuration to #{@s3_bucket}/#{key}\n#{configuration}")
|
347
|
+
$log.info(configuration.to_yaml)
|
348
|
+
@s3_client.put_object({
|
349
|
+
bucket: @s3_bucket,
|
350
|
+
key: key,
|
351
|
+
body: JSON.pretty_generate(configuration)
|
352
|
+
})
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cfn_manage
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Base2Services
|
8
|
+
- Nikola Tosic
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2017-09-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: aws-sdk
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '3'
|
21
|
+
- - "<"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '4'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '3'
|
31
|
+
- - "<"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4'
|
34
|
+
description: ''
|
35
|
+
email: info@base2services.com
|
36
|
+
executables:
|
37
|
+
- cfn_manage
|
38
|
+
extensions: []
|
39
|
+
extra_rdoc_files: []
|
40
|
+
files:
|
41
|
+
- bin/cfn_manage
|
42
|
+
- bin/cfn_manage.rb
|
43
|
+
- bin/usage.txt
|
44
|
+
- lib/aws_credentials.rb
|
45
|
+
- lib/cf_common.rb
|
46
|
+
- lib/cf_progress_tracker.rb
|
47
|
+
- lib/cf_start_stop_environment.rb
|
48
|
+
homepage: http://rubygems.org/gems/cfn_manage
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.6.12
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: Manage Cloud Formation stacks
|
72
|
+
test_files: []
|