Skip to content

Instantly share code, notes, and snippets.

@kaspergrubbe
Last active October 14, 2024 22:02
Show Gist options
  • Save kaspergrubbe/34bfb60dd016f486df6ecddfbff7dce8 to your computer and use it in GitHub Desktop.
Save kaspergrubbe/34bfb60dd016f486df6ecddfbff7dce8 to your computer and use it in GitHub Desktop.
Here's a Ruby script that deletes unused images (snapshots) on Hetzner

Delete unused Hetzner images

I write images on Hetzner using Packer, but it becomes quite a pain to clean them up manually, luckily there's an API, so I built a quick script that does two things:

  • Fetch all images in use by servers.
  • Fetch all available images on Hetzner.

It then compares the 2, and offers you to delete the unused images.

Usage:

ruby unused_hetzner_image_deletion.rb

Examples

When images to delete:

3 image(s) in use:
- 192188323: shared-server-2.0.3
- 192188339: nomad-client-0.1.10
- 192732826: buildkite-0.0.6

8 image(s) to delete:
- 180526387: nomad-client-0.1.1
- 180558880: nomad-client-0.1.3
- 180928223: nomad-client-0.1.5
- 182489275: shared-server-2.0.1
- 182491740: nomad-client-0.1.6
- 182508392: nomad-client-0.1.7
- 182630086: nomad-client-0.1.8
- 185031268: nomad-client-0.1.9

.. delete 8 images? (Y/n)Y
Deleting nomad-client-0.1.1..!
Deleting nomad-client-0.1.3..!
Deleting nomad-client-0.1.5..!
Deleting shared-server-2.0.1..!
Deleting nomad-client-0.1.6..!
Deleting nomad-client-0.1.7..!
Deleting nomad-client-0.1.8..!
Deleting nomad-client-0.1.9..!

When no images to delete:

3 image(s) in use:
- 192188323: shared-server-2.0.3
- 192188339: nomad-client-0.1.10
- 192732826: buildkite-0.0.6

No unused images
.. bye!
# A quick and dirty image deletion
API_KEY = ''
require 'uri'
require 'net/http'
require 'openssl'
require 'json'
def json_get(url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
request = Net::HTTP::Get.new(url)
request['Authorization'] = "Bearer #{API_KEY}"
response = http.request(request)
JSON.parse(response.read_body)
end
def delete(url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
request = Net::HTTP::Delete.new(url)
request['Authorization'] = "Bearer #{API_KEY}"
response = http.request(request)
raise "Error deleting #{url}" if response.code != '204'
response
end
Pagination = Struct.new(:page, :next_page) do
def more?
next_page != nil
end
end
def parse_pagination(request_json)
pagination = request_json['meta']['pagination']
Pagination.new.tap do |pagy|
pagy.page = pagination['page']
pagy.next_page = pagination['next_page']
end
end
Image = Struct.new(:id, :name, :description, :deleted, :type) do
def snapshot?
type == 'snapshot'
end
end
def parse_server_images(request_json)
[].tap do |images|
request_json['servers'].each do |server|
images << Image.new.tap do |image|
image.id = server['image']['id']
image.name = server['image']['labels']['imagename']
image.description = server['image']['description']
image.deleted = server['image']['deleted']
image.type = server['image']['type']
end
end
end
end
def parse_images(request_json)
[].tap do |images|
request_json['images'].each do |image_json|
images << Image.new.tap do |image|
image.id = image_json['id']
image.name = image_json['labels']['imagename']
image.description = image_json['description']
image.deleted = image_json['deleted']
image.type = image_json['type']
end
end
end
end
images_in_use = [].tap do |images|
page_id = 1
loop do
json = json_get(URI("https://api.hetzner.cloud/v1/servers?per_page=3&page=#{page_id}&status=running"))
images.concat(parse_server_images(json))
pagy = parse_pagination(json)
break unless pagy.more?
page_id += 1
end
end.uniq(&:id).select(&:snapshot?)
images_available = [].tap do |images|
page_id = 1
loop do
json = json_get(URI("https://api.hetzner.cloud/v1/images?per_page=3&page=#{page_id}&type=snapshot"))
images.concat(parse_images(json))
pagy = parse_pagination(json)
break unless pagy.more?
page_id += 1
end
end.select(&:snapshot?)
images_to_delete = [].tap do |images|
images_available.each do |available_image|
next if images_in_use.any? { |i| i.id == available_image.id }
images << available_image
end
end
puts "#{images_in_use.size} image(s) in use:"
images_in_use.each do |image|
puts "- #{image.id}: #{image.name}"
end
puts ''
if images_to_delete.any?
puts "#{images_to_delete.size} image(s) to delete:"
images_to_delete.each do |image|
puts "- #{image.id}: #{image.name}"
end
puts ''
print ".. delete #{images_to_delete.size} images? (Y/n)"
answer = gets.chomp
if answer == 'Y'
images_to_delete.each do |image|
print "Deleting #{image.name}.."
delete(URI("https://api.hetzner.cloud/v1/images/#{image.id}"))
print "!\n"
end
else
puts 'Bye!'
end
else
puts 'No unused images'
puts '.. bye!'
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment