cfn_manage 0.1.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/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: []
|