cfn_manage 0.7.1 → 0.8.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.
@@ -0,0 +1,89 @@
1
+ require 'aws-sdk-docdb'
2
+ require 'cfn_manage/aws_credentials'
3
+
4
+ module CfnManage
5
+ module StartStopHandler
6
+ class DocumentDb
7
+
8
+ def initialize(cluster_id, options = {})
9
+ @cluster_id = cluster_id
10
+ credentials = CfnManage::AWSCredentials.get_session_credentials("startstopcluster_#{cluster_id}")
11
+ @docdb_client = Aws::DocDB::Client.new(retry_limit: 20)
12
+ if credentials != nil
13
+ @docdb_client = Aws::DocDB::Client.new(credentials: credentials, retry_limit: 20)
14
+ end
15
+ cluster = @docdb_client.describe_db_clusters({ db_cluster_identifier: @cluster_id })
16
+ @docdb_cluster = cluster.db_clusters.first
17
+ end
18
+
19
+ def start(configuration)
20
+ if @docdb_cluster.status == 'available'
21
+ $log.info("DocDB Cluster #{@cluster_id} is already in available state")
22
+ return
23
+ end
24
+
25
+ # start docdb cluster
26
+ if @docdb_cluster.status == 'stopped'
27
+ $log.info("Starting DocDB cluster #{@cluster_id}")
28
+ @docdb_client.start_db_cluster({ db_cluster_identifier: @cluster_id })
29
+ unless CfnManage.skip_wait?
30
+ # wait cluster to become available
31
+ $log.info("Waiting DocDB cluster to become available #{@cluster_id}")
32
+ wait('available')
33
+ end
34
+ else
35
+ $log.info("DocDB Cluster #{@cluster_id} is not in a stopped state. State: #{@docdb_cluster.status}")
36
+ end
37
+ end
38
+
39
+ def stop
40
+ if @docdb_cluster.status == 'stopped'
41
+ $log.info("DocDB Cluster #{@cluster_id} is already stopped")
42
+ return {}
43
+ end
44
+
45
+ if @docdb_cluster.status != 'available'
46
+ $log.info("DocDB Cluster #{@cluster_id} is not in a available state. State: #{@docdb_cluster.status}")
47
+ return {}
48
+ end
49
+ # stop docdb cluster and wait for it to be fully stopped
50
+ $log.info("Stopping DocDB cluster #{@cluster_id}")
51
+ @docdb_client.stop_db_cluster({ db_cluster_identifier: @cluster_id })
52
+ unless CfnManage.skip_wait?
53
+ $log.info("Waiting DocDB cluster to be stopped #{@cluster_id}")
54
+ wait('stopped')
55
+ end
56
+ return {}
57
+ end
58
+
59
+ def wait(completed_state)
60
+ # reached state must be steady, at least a minute.
61
+ state_count = 0
62
+ steady_count = 4
63
+ attempts = 0
64
+
65
+ until attempts == (max_attempts = 60*6) do
66
+ # Declare client and cluster variable a second time inside the loop so it re-evaluates each time.
67
+ docdb = @docdb_client.describe_db_clusters({ db_cluster_identifier: @cluster_id })
68
+ cluster = docdb.db_clusters.first
69
+ $log.info("DocDB Cluster #{cluster.db_cluster_identifier} state: #{cluster.status}, waiting for #{completed_state}")
70
+
71
+ if cluster.status == "#{completed_state}"
72
+ state_count = state_count + 1
73
+ $log.info("#{state_count}/#{steady_count}")
74
+ else
75
+ state_count = 0
76
+ end
77
+ break if state_count == steady_count
78
+ attempts = attempts + 1
79
+ sleep(15)
80
+ end
81
+
82
+ if attempts == max_attempts
83
+ $log.error("DocDB Cluster #{@cluster_id} did not enter #{completed_state} state, however continuing operations...")
84
+ end
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,42 @@
1
+ require 'aws-sdk-ec2'
2
+ require 'cfn_manage/aws_credentials'
3
+
4
+ module CfnManage
5
+ module StartStopHandler
6
+ class Ec2
7
+
8
+ def initialize(instance_id, options = {})
9
+ credentials = CfnManage::AWSCredentials.get_session_credentials("stoprun_#{instance_id}")
10
+ ec2_client = Aws::EC2::Client.new(credentials: credentials, retry_limit: 20)
11
+ @instance = Aws::EC2::Resource.new(client: ec2_client, retry_limit: 20).instance(instance_id)
12
+ @instance_id = instance_id
13
+ end
14
+
15
+ def start(configuration)
16
+ if %w(running).include?(@instance.state.name)
17
+ $log.info("Instance #{@instance_id} already running")
18
+ return
19
+ end
20
+ $log.info("Starting instance #{@instance_id}")
21
+ @instance.start()
22
+ end
23
+
24
+ def stop
25
+ if %w(stopped stopping).include?(@instance.state.name)
26
+ $log.info("Instance #{@instance_id} already stopping or stopped")
27
+ return
28
+ end
29
+ $log.info("Stopping instance #{@instance_id}")
30
+ @instance.stop()
31
+
32
+ # empty configuration for ec2 instances
33
+ return {}
34
+ end
35
+
36
+ def wait(wait_states=[])
37
+ $log.debug("Not waiting for EC2 instance #{@instance_id}")
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,207 @@
1
+ require 'aws-sdk-ecs'
2
+ require 'cfn_manage/aws_credentials'
3
+
4
+ module CfnManage
5
+ module StartStopHandler
6
+ class EcsCluster
7
+
8
+ def initialize(cluster_id, options = {})
9
+ @wait_state = options.has_key?(:wait_state) ? options[:wait_state] : CfnManage.ecs_wait_state
10
+ @skip_wait = options.has_key?(:skip_wait) ? CfnManage.true?(options[:skip_wait]) : CfnManage.skip_wait?
11
+ @wait_container_instances = options.has_key?(:wait_container_instances) ? CfnManage.true?(options[:wait_container_instances]) : CfnManage.ecs_wait_container_instances?
12
+ @ignore_missing_ecs_config = options.has_key?(:ignore_missing_ecs_config) ? CfnManage.true?(options[:ignore_missing_ecs_config]) : CfnManage.ignore_missing_ecs_config?
13
+
14
+ credentials = CfnManage::AWSCredentials.get_session_credentials("stoprun_#{cluster_id}")
15
+ @ecs_client = Aws::ECS::Client.new(credentials: credentials, retry_limit: 20)
16
+ @elb_client = Aws::ElasticLoadBalancingV2::Client.new(credentials: credentials, retry_limit: 20)
17
+ @services = []
18
+ @ecs_client.list_services(cluster: cluster_id, scheduling_strategy: 'REPLICA', max_results: 100).each do |results|
19
+ @services.push(*results.service_arns)
20
+ end
21
+ $log.info("Found #{@services.count} services in ECS cluster #{cluster_id}")
22
+ @cluster = cluster_id
23
+ end
24
+
25
+ def start(configuration)
26
+ if @wait_container_instances
27
+ wait_for_instances()
28
+ end
29
+
30
+ @services.each do |service_arn|
31
+
32
+ $log.info("Searching for ECS service #{service_arn} in cluster #{@cluster}")
33
+ service = @ecs_client.describe_services(services:[service_arn], cluster: @cluster).services.first
34
+
35
+ if service.desired_count != 0
36
+ $log.info("ECS service #{service.service_name} is already running")
37
+ next
38
+ end
39
+
40
+ if configuration.has_key?(service.service_name)
41
+ desired_count = configuration[service.service_name]['desired_count']
42
+ elsif CfnManage.ignore_missing_ecs_config?
43
+ $log.info("ECS service #{service.service_name} wasn't previosly stopped by cfn_manage. Option --ignore-missing-ecs-config set and setting desired count to 1")
44
+ desired_count = 1
45
+ else
46
+ $log.warn("ECS service #{service.service_name} wasn't previosly stopped by cfn_manage. Skipping ...")
47
+ next
48
+ end
49
+
50
+ $log.info("Starting ECS service #{service.service_name} with desired count of #{desired_count}")
51
+ @ecs_client.update_service({
52
+ desired_count: desired_count,
53
+ service: service_arn,
54
+ cluster: @cluster
55
+ })
56
+
57
+ end
58
+
59
+ if desired_count == 0
60
+ # skip wait if desired count is purposfully set to 0
61
+ $log.info("Desired capacity is 0, skipping wait for ecs service #{service.service_name}")
62
+ elsif !@skip_wait
63
+ @services.each do |service_arn|
64
+ wait(@wait_state,service_arn)
65
+ end
66
+ end
67
+ end
68
+
69
+ def stop
70
+ configuration = {}
71
+ @services.each do |service_arn|
72
+
73
+ $log.info("Searching for ECS service #{service_arn} in cluster #{@cluster}")
74
+ service = @ecs_client.describe_services(services:[service_arn], cluster: @cluster).services.first
75
+
76
+ if service.desired_count == 0
77
+ $log.info("ECS service #{service.service_name} is already stopped")
78
+ next
79
+ end
80
+
81
+ configuration[service.service_name] = { desired_count: service.desired_count }
82
+ $log.info("Stopping ECS service #{service.service_name}")
83
+ @ecs_client.update_service({
84
+ desired_count: 0,
85
+ service: service_arn,
86
+ cluster: @cluster
87
+ })
88
+
89
+ end
90
+
91
+ return configuration.empty? ? nil : configuration
92
+ end
93
+
94
+ def wait(type,service_arn=nil)
95
+
96
+ if service_arn.nil?
97
+ $log.warn("unable to wait for #{service_arn} service")
98
+ return
99
+ end
100
+
101
+ attempts = 0
102
+
103
+ until attempts == (max_attempts = 60*6) do
104
+
105
+ case type
106
+ when 'Running'
107
+ success = wait_till_running(service_arn)
108
+ when 'HealthyInTargetGroup'
109
+ success = wait_till_healthy_in_target_group(service_arn)
110
+ else
111
+ $log.warn("unknown ecs service wait type #{type}. skipping...")
112
+ break
113
+ end
114
+
115
+ if success
116
+ break
117
+ end
118
+
119
+ attempts = attempts + 1
120
+ sleep(15)
121
+ end
122
+
123
+ if attempts == max_attempts
124
+ $log.error("Failed to wait for ecs service with wait type #{type}")
125
+ end
126
+ end
127
+
128
+ def wait_for_instances
129
+
130
+ attempts = 0
131
+
132
+ until attempts == (max_attempts = 60*3) do
133
+
134
+ resp = @ecs_client.list_container_instances({
135
+ cluster: @cluster,
136
+ status: "ACTIVE"
137
+ })
138
+
139
+ if resp.container_instance_arns.any?
140
+ $log.info("A container instances has joined ecs cluster #{@cluster}")
141
+ break
142
+ end
143
+
144
+ attempts = attempts + 1
145
+ sleep(5)
146
+ end
147
+
148
+ if attempts == max_attempts
149
+ $log.error("Failed to wait for container instances to join ecs cluster #{@cluster}")
150
+ end
151
+ end
152
+
153
+ def wait_till_running(service_arn)
154
+ service_name = service_arn.split('/').last
155
+ service = @ecs_client.describe_services(services:[service_arn], cluster: @cluster).services.first
156
+
157
+ if service.running_count > 0
158
+ $log.info("ecs service #{service_name} has #{service.running_count} running tasks")
159
+ return true
160
+ end
161
+
162
+ $log.info("waiting for ecs service #{service_name} to reach a running state")
163
+ return false
164
+ end
165
+
166
+ def wait_till_healthy_in_target_group(service_arn)
167
+ service = @ecs_client.describe_services(services:[service_arn], cluster: @cluster).services.first
168
+ target_groups = service.load_balancers.collect { |lb| lb.target_group_arn }
169
+
170
+ if target_groups.empty?
171
+ # we want to skip here if the asg is not associated with any target groups
172
+ $log.info("ecs aervice #{service_arn} is not associated with any target groups")
173
+ return true
174
+ end
175
+
176
+ target_health = []
177
+ target_groups.each do |tg|
178
+ resp = @elb_client.describe_target_health({
179
+ target_group_arn: tg,
180
+ })
181
+ if resp.target_health_descriptions.empty?
182
+ # we need to wait until a ecs task has been placed into the target group
183
+ # before we can check it's healthy
184
+ $log.info("ECS service #{service_arn} hasn't been placed into target group #{tg.split('/')[1]} yet")
185
+ return false
186
+ end
187
+ target_health.push(*resp.target_health_descriptions)
188
+ end
189
+
190
+ state = target_health.collect {|tg| tg.target_health.state}
191
+
192
+ if state.all? 'healthy'
193
+ $log.info("All ecs tasks are in a healthy state in target groups #{target_groups.map {|tg| tg.split('/')[1] }}")
194
+ return true
195
+ end
196
+
197
+ unhealthy = target_health.select {|tg| tg.target_health.state != 'healthy'}
198
+ unhealthy.each do |tg|
199
+ $log.info("waiting for ecs task #{tg.target.id} to be healthy in target group. Current state is #{tg.target_health.state}")
200
+ end
201
+
202
+ return false
203
+ end
204
+
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,134 @@
1
+ require 'aws-sdk-rds'
2
+ require 'cfn_manage/aws_credentials'
3
+
4
+ module CfnManage
5
+ module StartStopHandler
6
+ class Rds
7
+
8
+ def initialize(instance_id, options = {})
9
+ @instance_id = instance_id
10
+ @excluded_engines = %w(aurora aurora-mysql aurora-postgresql) # RDS list of exluded engines that don't support RDS stop start
11
+ credentials = CfnManage::AWSCredentials.get_session_credentials("startstoprds_#{instance_id}")
12
+ @rds_client = Aws::RDS::Client.new(retry_limit: 20)
13
+ if credentials != nil
14
+ @rds_client = Aws::RDS::Client.new(credentials: credentials, retry_limit: 20)
15
+ end
16
+ rds = Aws::RDS::Resource.new(client: @rds_client)
17
+ @rds_instance = rds.db_instance(instance_id)
18
+
19
+ end
20
+
21
+ def start(configuration)
22
+ if @excluded_engines.include? @rds_instance.engine
23
+ $log.info("RDS Instance #{@instance_id} engine is #{@rds_instance.engine} and cannot be started by instance.")
24
+ return
25
+ end
26
+
27
+ if @rds_instance.db_instance_status == 'available'
28
+ $log.info("RDS Instance #{@instance_id} is already in available state")
29
+ end
30
+
31
+ # start rds instance
32
+ if @rds_instance.db_instance_status == 'stopped'
33
+ $log.info("Starting db instance #{@instance_id}")
34
+ @rds_client.start_db_instance({ db_instance_identifier: @instance_id })
35
+
36
+ # wait instance to become available
37
+ unless CfnManage.skip_wait?
38
+ $log.info("Waiting db instance to become available #{@instance_id}")
39
+ wait('available')
40
+ end
41
+ else
42
+ wait('available') unless CfnManage.skip_wait?
43
+ end
44
+
45
+ # convert rds instance to mutli-az if required
46
+ if configuration['is_multi_az']
47
+ $log.info("Converting to Multi-AZ instance after start (instance #{@instance_id})")
48
+ set_rds_instance_multi_az( true)
49
+ end unless configuration.nil?
50
+ end
51
+
52
+ def stop
53
+
54
+ configuration = {
55
+ is_multi_az: @rds_instance.multi_az
56
+ }
57
+ # RDS list of exluded engines that don't support RDS stop start
58
+ if @excluded_engines.include? @rds_instance.engine
59
+ $log.info("RDS Instance #{@instance_id} engine is #{@rds_instance.engine} and cannot be stoped by instance.")
60
+ return configuration
61
+ end
62
+
63
+ # check if available
64
+ if @rds_instance.db_instance_status != 'available'
65
+ $log.warn("RDS Instance #{@instance_id} not in available state, and thus can not be stopped")
66
+ $log.warn("RDS Instance #{@instance_id} state: #{@rds_instance.db_instance_status}")
67
+ return configuration
68
+ end
69
+
70
+ # check if already stopped
71
+ if @rds_instance.db_instance_status == 'stopped'
72
+ $log.info("RDS Instance #{@instance_id} is already stopped")
73
+ return configuration
74
+ end
75
+
76
+ #check if mutli-az RDS. if so, convert to single-az
77
+ if @rds_instance.multi_az
78
+ $log.info("Converting to Non-Multi-AZ instance before stop (instance #{@instance_id}")
79
+ set_rds_instance_multi_az(false)
80
+ end
81
+
82
+ # stop rds instance and wait for it to be fully stopped
83
+ $log.info("Stopping instance #{@instance_id}")
84
+ @rds_client.stop_db_instance({ db_instance_identifier: @instance_id })
85
+ unless CfnManage.skip_wait?
86
+ $log.info("Waiting db instance to be stopped #{@instance_id}")
87
+ wait('stopped')
88
+ end
89
+
90
+ return configuration
91
+ end
92
+
93
+ def set_rds_instance_multi_az(multi_az)
94
+ if @rds_instance.multi_az == multi_az
95
+ $log.info("Rds instance #{@rds_instance.db_instance_identifier} already multi-az=#{multi_az}")
96
+ return
97
+ end
98
+ @rds_instance.modify({ multi_az: multi_az, apply_immediately: true })
99
+ # allow half an hour for instance to be converted
100
+ wait('available')
101
+ end
102
+
103
+ def wait(completed_state)
104
+ # reached state must be steady, at least a minute. Modifying an instance to/from MultiAZ can't be shorter
105
+ # than 40 seconds, hence steady count is 4
106
+ state_count = 0
107
+ steady_count = 4
108
+ attempts = 0
109
+ rds = Aws::RDS::Resource.new(client: @rds_client)
110
+ until attempts == (max_attempts = 60*6) do
111
+ instance = rds.db_instance(@instance_id)
112
+ $log.info("Instance #{instance.db_instance_identifier} state: #{instance.db_instance_status}, waiting for #{completed_state}")
113
+
114
+ if instance.db_instance_status == "#{completed_state}"
115
+ state_count = state_count + 1
116
+ $log.info("#{state_count}/#{steady_count}")
117
+ else
118
+ state_count = 0
119
+ end
120
+ break if state_count == steady_count
121
+ attempts = attempts + 1
122
+ sleep(15)
123
+ end
124
+
125
+ if attempts == max_attempts
126
+ $log.error("RDS Database Instance #{@instance_id} did not enter #{state} state, however continuing operations...")
127
+ end
128
+ end
129
+
130
+ private :set_rds_instance_multi_az
131
+
132
+ end
133
+ end
134
+ end