Skip to content

Instantly share code, notes, and snippets.

@alexanderadam
Created February 12, 2025 09:59
Show Gist options
  • Save alexanderadam/27a8b7488b9248f8b0be65f065d2472f to your computer and use it in GitHub Desktop.
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
#!/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