Created
February 12, 2025 09:59
-
-
Save alexanderadam/27a8b7488b9248f8b0be65f065d2472f to your computer and use it in GitHub Desktop.
Simple Ruby script to check when the data in an Exoscale S3 bucket was last modified
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
#!/usr/bin/env ruby | |
# frozen_string_literal: true | |
require 'bundler/inline' | |
gemfile do | |
source 'https://rubygems.org' | |
gem 'aws-sdk-s3', '~> 1.0' | |
gem 'terminal-table', '~> 3.0' | |
gem 'colorize', '~> 0.8' | |
gem 'logger' | |
gem 'ox' | |
end | |
require 'aws-sdk-s3' | |
require 'terminal-table' | |
require 'time' | |
require 'optparse' | |
require 'colorize' | |
EXOSCALE_REGIONS_MAPPING = { | |
'ch-dk-2' => '🇨🇭 Switzerland (Zürich)', | |
'de-muc-1' => '🇩🇪 Germany (Munich)', | |
'ch-gva-2' => '🇨🇭 Switzerland (Geneva)', | |
'at-vie-1' => '🇦🇹 Austria (Vienna)', | |
'de-fra-1' => '🇩🇪 Germany (Frankfurt)', | |
'bg-sof-1' => '🇧🇬 Bulgaria (Sofia)', | |
'at-vie-2' => '🇦🇹 Austria (Vienna 2)' | |
}.freeze | |
EXOSCALE_REGIONS = EXOSCALE_REGIONS_MAPPING.keys.freeze | |
def parse_options | |
options = {} | |
OptionParser.new do |opts| | |
opts.banner = "Usage: #{File.basename($0)} [options] BUCKET_NAME...".bold | |
opts.on('-k', '--access-key KEY', 'Exoscale Access Key') do |key| | |
options[:access_key] = key | |
end | |
opts.on('-s', '--secret-key SECRET', 'Exoscale Secret Key') do |secret| | |
options[:secret_key] = secret | |
end | |
opts.on('-r', '--region REGION', "Exoscale Region (available: #{EXOSCALE_REGIONS_MAPPING.map { |k,v| "#{k} (#{v})"}.join(', ')})") do |region| | |
unless EXOSCALE_REGIONS.include?(region) | |
puts "❌ Invalid region: #{region}".red | |
puts "\nAvailable regions:".blue | |
EXOSCALE_REGIONS_MAPPING.each do |region, desc| | |
puts " #{region.ljust(10)} #{desc}".blue | |
end | |
exit 1 | |
end | |
options[:region] = region | |
end | |
opts.on('-v', '--verbose', 'Show verbose error messages') do | |
options[:verbose] = true | |
end | |
opts.on('-h', '--help', 'Show this help') do | |
puts "🪣 S3 Bucket Access Check".bold.blue | |
puts opts | |
exit | |
end | |
end.parse! | |
%i[access_key secret_key].each do |key| | |
unless options[key] | |
if ENV["EXOSCALE_#{key.upcase}"] | |
options[key] = ENV["EXOSCALE_#{key.upcase}"] | |
else | |
puts "❌ Error: Missing #{key}. Provide it via option or EXOSCALE_#{key.upcase} environment variable.".red | |
exit 1 | |
end | |
end | |
end | |
[options, ARGV] | |
end | |
def try_connect_region(options, region) | |
endpoint = "https://sos-%{region}.exo.io" % { region: region } | |
puts " 🔄 Testing endpoint: #{endpoint}".blue if options[:verbose] | |
Aws.config.update({ | |
credentials: Aws::Credentials.new(options[:access_key], options[:secret_key]), | |
region: region, | |
endpoint: endpoint, | |
force_path_style: true, | |
http_read_timeout: 10, | |
http_open_timeout: 5 | |
}) | |
print " Trying #{region} #{EXOSCALE_REGIONS_MAPPING[region].split(' ').first}... ".yellow | |
begin | |
s3_client = Aws::S3::Client.new | |
s3_client.list_buckets | |
puts "✅ Connected!".green | |
[s3_client, region] | |
rescue Aws::S3::Errors::PermanentRedirect => e | |
puts "⚠️ Wrong region".yellow | |
nil | |
rescue Seahorse::Client::NetworkingError => e | |
error_details = case e.message | |
when /connection refused/i | |
"Connection refused - service might be down" | |
when /name or service not known/i | |
"DNS resolution failed - check endpoint URL" | |
when /timeout/i | |
"Connection timed out - check network connectivity" | |
when /certificate/i | |
"SSL/TLS error - certificate validation failed" | |
else | |
e.message | |
end | |
puts " ❌ Network error: #{error_details}".red | |
puts " → Full error: #{e.class}: #{e.message}".light_black if options[:verbose] | |
nil | |
rescue Aws::Errors::MissingCredentialsError | |
puts " ❌ Invalid credentials".red | |
nil | |
rescue Aws::S3::Errors::InvalidAccessKeyId | |
puts " ❌ Invalid access key".red | |
nil | |
rescue Aws::S3::Errors::SignatureDoesNotMatch | |
puts " ❌ Invalid secret key".red | |
nil | |
rescue => e | |
puts " ❌ Unexpected error: #{e.class}: #{e.message}".red | |
puts " → Backtrace: #{e.backtrace[0..2].join("\n\t")}".light_black if options[:verbose] | |
nil | |
end | |
end | |
def find_working_client(options) | |
if options[:region] | |
client, region = try_connect_region(options, options[:region]) | |
unless client | |
puts "\n❌ Could not connect to specified region #{options[:region]} #{EXOSCALE_REGIONS_MAPPING[options[:region]]}".red | |
puts "\nTry one of these regions:".blue | |
EXOSCALE_REGIONS_MAPPING.each do |region, desc| | |
puts " #{region.ljust(10)} #{desc}".blue | |
end | |
exit 1 | |
end | |
[client, region] | |
else | |
puts "🔄 No region specified, trying all available regions...".blue | |
EXOSCALE_REGIONS.each do |region| | |
result = try_connect_region(options, region) | |
return result if result | |
end | |
puts "\n❌ Could not connect to any Exoscale region:".red | |
puts " • Check your internet connection".yellow | |
puts " • Verify your credentials".yellow | |
puts " • Ensure the Exoscale S3 service is available".yellow | |
exit 1 | |
end | |
end | |
def get_bucket_info(s3_client, bucket_name) | |
begin | |
location = s3_client.get_bucket_location(bucket: bucket_name).location_constraint | |
last_modified = nil | |
s3_client.list_objects_v2(bucket: bucket_name).each do |response| | |
response.contents.each do |object| | |
last_modified = object.last_modified if last_modified.nil? || object.last_modified > last_modified | |
end | |
end | |
[bucket_name, location, last_modified] | |
rescue Aws::S3::Errors::NoSuchBucket | |
[bucket_name, nil, "⚠️ Bucket not found".yellow] | |
rescue Aws::S3::Errors::AccessDenied | |
[bucket_name, nil, "🔒 Access denied".red] | |
rescue => e | |
[bucket_name, nil, "❌ Error: #{e.message}".red] | |
end | |
end | |
def format_last_access(last_modified) | |
return last_modified unless last_modified.is_a?(Time) | |
days_ago = (Time.now - last_modified) / (24 * 60 * 60) | |
time_str = last_modified.strftime('%Y-%m-%d %H:%M:%S UTC') | |
case days_ago | |
when 0..1 | |
"🟢 #{time_str}".green | |
when 2..7 | |
"🟡 #{time_str}".yellow | |
when 8..30 | |
"🟠 #{time_str}".light_red | |
else | |
"🔴 #{time_str}".red | |
end | |
end | |
def main | |
options, buckets = parse_options | |
begin | |
s3_client, connected_region = find_working_client(options) | |
flag = EXOSCALE_REGIONS_MAPPING[connected_region].split(' ').first | |
puts "\n🌍 Connected to region: #{connected_region} #{flag}".blue | |
if buckets.empty? | |
puts "ℹ️ No buckets specified, listing all buckets…".blue | |
buckets = s3_client.list_buckets.buckets.map(&:name) | |
if buckets.empty? | |
puts "⚠️ No buckets found".yellow | |
exit 0 | |
end | |
end | |
puts "\n🔍 Checking buckets in #{connected_region}…".blue | |
table = Terminal::Table.new do |t| | |
t.title = "Bucket Access Information".bold | |
t.headings = ['Bucket Name'.bold, 'Region'.bold, 'Last Access'.bold] | |
t.style = { border_x: "─".light_black, border_y: "│".light_black, border_i: "┼".light_black } | |
buckets.each do |bucket| | |
name, location, last_modified = get_bucket_info(s3_client, bucket) | |
location_str = location ? "🌍 #{location}".cyan : "❓ N/A".light_black | |
last_access = format_last_access(last_modified) | |
t << [name.bold, location_str, last_access] | |
end | |
end | |
puts table | |
puts "\n💡 Legend:".bold | |
puts "🟢 Recent (< 1 day) | 🟡 Week old | 🟠 Month old | 🔴 Older".light_black | |
rescue Aws::Errors::MissingCredentialsError | |
puts "❌ Error: Missing or invalid credentials".red | |
exit 1 | |
rescue => e | |
puts "❌ Error: #{e.message}".red | |
exit 1 | |
end | |
end | |
main if $0 == __FILE__ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment