Skip to content

Instantly share code, notes, and snippets.

@quark-zju
Created June 2, 2015 14:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save quark-zju/77d261c1e5c40e4863e1 to your computer and use it in GitHub Desktop.
Save quark-zju/77d261c1e5c40e4863e1 to your computer and use it in GitHub Desktop.
Poor man's qingcloud single instance management script
#!/usr/bin/env ruby
# qingcloud-control
#
# Poor man's qingcloud single instance management script. You may find this script useful, if you:
# - are an individual qingcloud user. do not have a lot of instances (assuming only one)
# - do not need to run instance 7x24. instances are powered off most of the time
# - do not use advanced networks. no routers, no private networks. just an instance with default network and an eip attached
# - want to save money
# - use ssh to login, have following lines in ~/.ssh/config:
#
# Host qingcloud-myinstance
# # myinstance::ip
# Hostname 111.222.333.444
# IdentityFile ...
# ...
#
# # after setting up official "qingcloud" cli tool
# qingcloud-control stop # stop instance, delete eip
# qingcloud-control start # start instance (or create from snapshot). allocate and attach eip on demand, update IP address in .ssh/config. remove related snapshot [1]
# qingcloud-control hibernate # stop instance. create snapshot. delete instance. this costs only 1/3 of "stop" but "start" will be slow
# qingcloud-control status # query and print status
#
# How to use?
# 0. Make sure "qingcloud" cli works
# 1. Create file .env like:
# INSTANCE=foo
# KEYPAIR=kp-abcd
# 2. Create a qingcloud instance named "foo"
# 3. Try "qingcloud-control stop"; "qingcloud-control hibernate"
# 4. Try "qingcloud-control start"
# 5. Enjoy :)
#
require 'json'
require 'logger'
LOG = Logger.new(STDERR)
if File.exists?('.env')
File.read('.env').each_line do |line|
k, v = line.chomp.split('=')
ENV[k] = v
end
end
# Note: not escape these names from ENV. ENV must be trusted.
INSTANCE_NAME = ENV['INSTANCE'] || 'myinstance'
EIP_NAME = "#{INSTANCE_NAME}::ip"
SNAPSHOT_NAME = "#{INSTANCE_NAME}::ss"
IMAGE_NAME = "#{INSTANCE_NAME}::img"
KEEP_EIP = ENV['KEEP_EIP']
# params used when creating resources
EIP_PARAMS = '-B bandwidth -b 1'
INSTANCE_PAAMS = '-t c1m1 -n vxnet-0 ' + (ENV['KEYPAIR'] ? "-l keypair -k #{ENV['KEYPAIR']}" : '-l passwd -p PASSW0rd')
def qingcloud(command)
LOG.debug command
JSON.parse(`qingcloud iaas #{command}`).tap do |result|
if result['ret_code'].to_i != 0
raise "Failed to run #{command}: #{result}"
end
end
end
def wait_for(message, timeout = 120)
timeout.times do
return if yield
sleep 1
end
raise "Timed out waiting #{message}"
end
def with_retry(n = 60)
begin
return yield
rescue => ex
n -= 1
raise ex if n == 0
sleep 1
retry
end
end
def not_nil(x, message = 'nil is not expected')
raise message if x.nil?
x
end
def find_instance
qingcloud("describe-instances -s pending,running,stopped,suspended -W #{INSTANCE_NAME}")['instance_set'].first
end
def find_instance!
not_nil find_instance, "Instance #{INSTANCE_NAME} not found"
end
def find_eip
qingcloud("describe-eips -s pending,available,associated,suspended -W #{EIP_NAME}")['eip_set'].first
end
def find_eip!
not_nil find_eip, "EIP #{EIP_NAME} not found"
end
def find_snapshot
qingcloud("describe-snapshots -s pending,available,suspended -W #{SNAPSHOT_NAME}")['snapshot_set'].first
end
def find_snapshot!
not_nil find_snapshot, "Snapshot #{SNAPSHOT_NAME} not found"
end
def find_image
qingcloud("describe-images -s pending,available -W #{IMAGE_NAME}")['image_set'].first
end
def find_image!
not_nil find_image, "Image #{INSTANCE_NAME} not found"
end
def start
instance = find_instance
image = find_image
snapshot = find_snapshot
# If the instance does not exist, create it from snapshot
if instance.nil?
# snapshot -> image
if snapshot.nil?
raise "No snapshot can be used to create instance"
end
if image.nil?
qingcloud("capture-instance-from-snapshot -s #{snapshot['snapshot_id']} -N #{IMAGE_NAME}")
wait_for('Image is ready') do
image = find_image
image && image['status'] == 'available'
end
end
# image -> instance
qingcloud("run-instances -N #{INSTANCE_NAME} -m #{image['image_id']} #{INSTANCE_PAAMS}")
wait_for('Instance is ready') do
instance = find_instance
instance && instance['status'] == 'running'
end
end
# Make sure the instance is running
if instance['status'] != 'running'
qingcloud("start-instances -i #{instance['instance_id']}")
end
# Make sure it has an IP
eip = find_eip
if eip.nil?
qingcloud("allocate-eips -n #{EIP_NAME} #{EIP_PARAMS}")
eip = find_eip!
end
if eip['status'] == 'available'
# Bind IP to instance
wait_for 'Instance becomes running' do
find_instance['status'] == 'running'
end
qingcloud("associate-eip -e #{eip['eip_id']} -i #{instance['instance_id']}")
elsif eip['status'] == 'associated'
# Associated to instance
if eip['resource']['resource_id'] != instance['instance_id']
raise "EIP #{eip['eip_id']} not associated to #{instance['instance_id']}"
end
else
raise "Unexpected eip status: #{eip['status']}"
end
LOG.info "EIP: #{eip['eip_addr']}"
# update ssh_config
ssh_config_path = File.expand_path('~/.ssh/config')
if File.readable?(ssh_config_path)
ssh_config = File.read(ssh_config_path)
ssh_config[/# #{EIP_NAME}[^H]*Hostname ([0-9\.]+)/i, 1] = eip['eip_addr']
File.write(ssh_config_path, ssh_config)
end
# cleanup: delete image and snapshot
if image
# We may got: PermissionDenied, resource [img-sh9qufsj] lease info not ready yet, please try later
with_retry do
qingcloud("delete-images -i #{image['image_id']}")
end
end
if snapshot
with_retry do
qingcloud("delete-snapshots -s #{snapshot['snapshot_id']}")
end
end
end
def stop
instance = find_instance
eip = find_eip
if eip && KEEP_EIP.to_i == 0
if eip['status'] == 'associated'
qingcloud("dissociate-eips -e #{eip['eip_id']}")
wait_for "EIP #{eip['eip_id']} becomes available", 60 do
find_eip['status'] == 'available'
end
end
end
if instance && instance['status'] == 'running' && instance['transition_status'] != 'stopping'
qingcloud("stop-instances -i #{instance['instance_id']}")
end
if eip && KEEP_EIP.to_i == 0
with_retry do
# We may got: PermissionDenied, resource [eip-516pigmi] lease info not ready yet, please try later
qingcloud("release-eips -F 1 -e #{eip['eip_id']}")
end
end
end
def hibernate
stop
snapshot = find_snapshot
instance = find_instance
image = find_image
qingcloud("delete-images -i #{image['image_id']}") if image
if snapshot.nil? && instance
# Create snapshot from stopped instance
wait_for "Instance #{instance['instance_id']} is power off", 60 do
find_instance['status'] == 'stopped'
end
qingcloud("create-snapshots -r #{instance['instance_id']} -N #{SNAPSHOT_NAME} -F 1")
wait_for "Snapshot #{SNAPSHOT_NAME} becomes available", 60 do
snapshot = find_snapshot
snapshot['status'] == 'available'
end
end
if snapshot && snapshot['status'] == 'available' && instance
# Delete instance
wait_for 'Instance is power off' do
find_instance['status'] == 'stopped'
end
qingcloud("terminate-instances -i #{instance['instance_id']}")
end
end
def status
instance = find_instance
if instance
puts "Instance: #{instance['instance_id']} # #{instance['status']}"
eip = find_eip
puts "EIP: #{eip['eip_addr']}" if eip
else
snapshot = find_snapshot
puts "Snapshot: #{snapshot['snapshot_id']}" if snapshot
end
end
CMD_MAP = {
'start' => proc do start end,
'stop' => proc do stop end,
'hibernate' => proc do hibernate end,
'status' => proc do status end,
'console' => proc do require 'pry'; binding.pry end,
}
cmd = CMD_MAP[ARGV[0]]
if cmd.nil?
puts "#$0 #{CMD_MAP.keys.join(' | ')}"
else
cmd.call
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment