Created
June 2, 2015 14:38
-
-
Save quark-zju/77d261c1e5c40e4863e1 to your computer and use it in GitHub Desktop.
Poor man's qingcloud single instance management script
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 | |
# 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