-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fe5e2d2
commit 3762f68
Showing
24 changed files
with
509 additions
and
234 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
.DS_Store | ||
.bundle/* | ||
src/.bundle/* | ||
src/vendor/* | ||
vendor/* | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
--color | ||
--require spec_helper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
Metrics/AbcSize: | ||
Enabled: false | ||
|
||
Metrics/BlockLength: | ||
Enabled: true | ||
Exclude: | ||
- spec/**/* | ||
|
||
Metrics/LineLength: | ||
Enabled: false | ||
|
||
Metrics/MethodLength: | ||
Enabled: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# frozen_string_literal: true | ||
|
||
source 'https://rubygems.org' | ||
|
||
ruby '2.5.0' | ||
|
||
gem 'activesupport' | ||
gem 'aws-sdk-rds' | ||
gem 'bundler' | ||
gem 'logger' | ||
gem 'tzinfo' | ||
gem 'tzinfo-data' | ||
|
||
group :test do | ||
gem 'rspec' | ||
gem 'rubocop' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
GEM | ||
remote: https://rubygems.org/ | ||
specs: | ||
activesupport (5.2.2) | ||
concurrent-ruby (~> 1.0, >= 1.0.2) | ||
i18n (>= 0.7, < 2) | ||
minitest (~> 5.1) | ||
tzinfo (~> 1.1) | ||
ast (2.4.0) | ||
aws-eventstream (1.0.1) | ||
aws-partitions (1.127.0) | ||
aws-sdk-core (3.44.1) | ||
aws-eventstream (~> 1.0) | ||
aws-partitions (~> 1.0) | ||
aws-sigv4 (~> 1.0) | ||
jmespath (~> 1.0) | ||
aws-sdk-rds (1.42.0) | ||
aws-sdk-core (~> 3, >= 3.39.0) | ||
aws-sigv4 (~> 1.0) | ||
aws-sigv4 (1.0.3) | ||
concurrent-ruby (1.1.4) | ||
diff-lcs (1.3) | ||
i18n (1.4.0) | ||
concurrent-ruby (~> 1.0) | ||
jaro_winkler (1.5.1) | ||
jmespath (1.4.0) | ||
logger (1.3.0) | ||
minitest (5.11.3) | ||
parallel (1.12.1) | ||
parser (2.5.3.0) | ||
ast (~> 2.4.0) | ||
powerpack (0.1.2) | ||
rainbow (3.0.0) | ||
rspec (3.8.0) | ||
rspec-core (~> 3.8.0) | ||
rspec-expectations (~> 3.8.0) | ||
rspec-mocks (~> 3.8.0) | ||
rspec-core (3.8.0) | ||
rspec-support (~> 3.8.0) | ||
rspec-expectations (3.8.2) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.8.0) | ||
rspec-mocks (3.8.0) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.8.0) | ||
rspec-support (3.8.0) | ||
rubocop (0.62.0) | ||
jaro_winkler (~> 1.5.1) | ||
parallel (~> 1.10) | ||
parser (>= 2.5, != 2.5.1.1) | ||
powerpack (~> 0.1) | ||
rainbow (>= 2.2.2, < 4.0) | ||
ruby-progressbar (~> 1.7) | ||
unicode-display_width (~> 1.4.0) | ||
ruby-progressbar (1.10.0) | ||
thread_safe (0.3.6) | ||
tzinfo (1.2.5) | ||
thread_safe (~> 0.1) | ||
tzinfo-data (1.2018.9) | ||
tzinfo (>= 1.0.0) | ||
unicode-display_width (1.4.1) | ||
|
||
PLATFORMS | ||
ruby | ||
|
||
DEPENDENCIES | ||
activesupport | ||
aws-sdk-rds | ||
bundler | ||
logger | ||
rspec | ||
rubocop | ||
tzinfo | ||
tzinfo-data | ||
|
||
RUBY VERSION | ||
ruby 2.5.0p0 | ||
|
||
BUNDLED WITH | ||
1.16.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
|
||
require_relative 'rds_scheduler' | ||
|
||
def main(*) | ||
RDSScheduler.new.execute | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'aws-sdk-rds' | ||
|
||
# Make calls to the AWS RDS API | ||
class RDSHelper | ||
def initialize(dry_run: false, logger: nil) | ||
@rds_client = Aws::RDS::Client.new | ||
@logger = logger | ||
@dry_run = dry_run | ||
end | ||
|
||
def db_instances | ||
@rds_client.describe_db_instances.db_instances | ||
end | ||
|
||
def db_tags(db_arn) | ||
@rds_client.list_tags_for_resource( | ||
resource_name: db_arn | ||
).tag_list | ||
end | ||
|
||
def start_db_instance(db_name, db_status, db_schedule) | ||
if db_status.eql? 'stopped' | ||
@logger.info "Starting DB Instance '#{db_name}' (schedule: '#{db_schedule}', dry_run: #{@dry_run})" | ||
@rds_client.start_db_instance(db_instance_identifier: db_name) unless @dry_run | ||
elsif db_status.eql? 'available' | ||
@logger.info "DB Instance '#{db_name}' is currently available (schedule: '#{db_schedule}')" | ||
else | ||
@logger.warn "DB Instance '#{db_name}' is not in a stopped state, not taking action (status: #{db_status}, schedule: '#{db_schedule}')" | ||
end | ||
end | ||
|
||
def stop_db_instance(db_name, db_status, db_schedule) | ||
if %w[stopping stopped].include?(db_status) | ||
@logger.info "DB Instance '#{db_name}' is currently stopped (status: #{db_status}, schedule: '#{db_schedule}')" | ||
elsif db_status.eql? 'available' | ||
@logger.info "Stopping DB Instance '#{db_name}' (schedule: '#{db_schedule}', dry_run: #{@dry_run})" | ||
@rds_client.stop_db_instance(db_instance_identifier: db_name) unless @dry_run | ||
else | ||
@logger.warn "DB Instance '#{db_name}' is not in a running state, not taking action (status: #{db_status}, schedule: '#{db_schedule}')" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'bundler' | ||
Bundler.setup | ||
require 'aws-sdk-rds' | ||
require 'logger' | ||
require_relative 'rds_helper' | ||
require_relative 'time_schedule_parser' | ||
|
||
# Manage uptime schedules for RDS Instances | ||
class RDSScheduler | ||
def initialize | ||
@logger = Logger.new(STDOUT) | ||
$stdout.sync = true | ||
dry_run = ENV.fetch('DRY_RUN', false).to_s.casecmp('true').zero? | ||
@loop_interval = ENV.fetch('LOOP_INTERVAL_SECS', 60).to_i | ||
@rds_client = RDSHelper.new(dry_run: dry_run, logger: @logger) | ||
@run_once = ENV.fetch('RUN_ONCE', false).to_s.casecmp('true').zero? | ||
@tag_uptime_schedule = ENV.fetch('TAG_UPTIME_SCHEDULE', 'appvia.io/rds-scheduler/uptime-schedule') | ||
@time_parser = TimeScheduleParser.new | ||
end | ||
|
||
def execute | ||
loop do | ||
@logger.info 'Retrieving DB instances...' | ||
dbs = @rds_client.db_instances | ||
|
||
dbs.each do |rds| | ||
tags = @rds_client.db_tags(rds.db_instance_arn) | ||
|
||
db_schedule = false | ||
tags.each do |tag| | ||
(db_schedule = tag.value) && break if tag.key.eql?(@tag_uptime_schedule) | ||
end | ||
|
||
process_schedule(rds.db_instance_identifier, rds.db_instance_status, db_schedule) | ||
end | ||
|
||
break if @run_once | ||
|
||
@logger.info("Sleeping for #{@loop_interval} seconds...") | ||
sleep(@loop_interval) | ||
end | ||
end | ||
|
||
def process_schedule(db_name, db_status, db_schedule) | ||
if db_schedule | ||
begin | ||
parsed_schedule = @time_parser.parse_schedule(db_schedule) | ||
rescue StandardError => e | ||
@logger.warn "DB Instance '#{db_name}' has an invalid schedule: #{e.message}" | ||
else | ||
begin | ||
if @time_parser.schedule_active?(parsed_schedule) | ||
@rds_client.start_db_instance(db_name, db_status, db_schedule) | ||
else | ||
@rds_client.stop_db_instance(db_name, db_status, db_schedule) | ||
end | ||
rescue TimeScheduleParser::TimezoneInvalid => e | ||
@logger.error "Error processing Time Schedule for DB Instance '#{db_name}': #{e.message}" | ||
end | ||
end | ||
else | ||
@logger.info "DB Instance '#{db_name}' has no schedule defined (status: #{db_status})" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
|
||
require_relative 'rds_scheduler' | ||
|
||
RDSScheduler.new.execute |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'active_support/core_ext/numeric/time' | ||
require 'tzinfo' | ||
|
||
# Parse time definition strings for validity | ||
class TimeScheduleParser | ||
WEEKDAYS = %w[MON TUE WED THU FRI SAT SUN].freeze | ||
TimeSchedule = Struct.new(:day_from, :day_to, :hour_from, :minute_from, :hour_to, :minute_to, :timezone) | ||
|
||
def time_schedule(opts) | ||
TimeSchedule.new(opts[:day_from], opts[:day_to], opts[:hour_from], opts[:minute_from], opts[:hour_to], opts[:minute_to], opts[:timezone]) | ||
end | ||
|
||
def parse_schedule(schedule) | ||
# Example: "Mon-Fri 09:00-17:30 Europe/London" | ||
regexp = %r{^([a-zA-Z]{3})-([a-zA-Z]{3}) (\d\d):(\d\d)-(\d\d):(\d\d) ([a-zA-Z/_]+)$} | ||
matches = schedule.match(regexp) | ||
|
||
raise 'Schedule does not match regex' if matches.nil? | ||
raise TimeScheduleParser::TimezoneInvalid, "Timezone is invalid: '#{matches.captures[6]}'" unless TZInfo::Timezone.all_identifiers.include?(matches.captures[6]) | ||
|
||
day_from = matches.captures[0].upcase | ||
day_to = matches.captures[1].upcase | ||
raise "Day Range is invalid: '#{day_from}-#{day_to}'" unless ([day_from, day_to] - WEEKDAYS).empty? | ||
|
||
time_schedule( | ||
day_from: day_from, | ||
day_to: day_to, | ||
hour_from: matches.captures[2].to_i, | ||
minute_from: matches.captures[3].to_i, | ||
hour_to: matches.captures[4].to_i, | ||
minute_to: matches.captures[5].to_i, | ||
timezone: matches.captures[6] | ||
) | ||
end | ||
|
||
def schedule_active?(schedule) | ||
begin | ||
current_time = Time.now.in_time_zone(schedule[:timezone]) | ||
rescue StandardError | ||
raise TimeScheduleParser::TimezoneInvalid, "Current time could not be computed with the given timezone: '#{schedule[:timezone]}'" | ||
end | ||
|
||
day_matches = WEEKDAYS.index(current_time.strftime('%a').upcase).between?(WEEKDAYS.index(schedule[:day_from]), WEEKDAYS.index(schedule[:day_to])) | ||
current_minutes = (current_time.hour * 60) + current_time.min | ||
time_matches = current_minutes.between?((schedule[:hour_from] * 60) + schedule[:minute_from], (schedule[:hour_to] * 60) + schedule[:minute_to]) | ||
day_matches && time_matches | ||
end | ||
|
||
class TimezoneInvalid < StandardError; end | ||
end |
Oops, something went wrong.