629 lines
18 KiB
Ruby
Executable File
629 lines
18 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
############################################################################
|
|
# Environment Configuration
|
|
############################################################################
|
|
ONE_LOCATION = ENV['ONE_LOCATION']
|
|
|
|
if !ONE_LOCATION
|
|
RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
|
|
ONEFLOW_LOCATION = '/usr/lib/one/oneflow/lib'
|
|
GEMS_LOCATION = '/usr/share/one/gems'
|
|
else
|
|
RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
|
|
ONEFLOW_LOCATION = ONE_LOCATION + '/lib/oneflow/lib'
|
|
GEMS_LOCATION = ONE_LOCATION + '/share/gems'
|
|
end
|
|
|
|
warn_level = $VERBOSE
|
|
$VERBOSE = nil
|
|
if File.directory?(GEMS_LOCATION)
|
|
real_gems_path = File.realpath(GEMS_LOCATION)
|
|
if !defined?(Gem) || Gem.path != [real_gems_path]
|
|
$LOAD_PATH.reject! {|l| l =~ /vendor_ruby/ }
|
|
require 'rubygems'
|
|
Gem.use_paths(real_gems_path)
|
|
end
|
|
end
|
|
$VERBOSE = warn_level
|
|
|
|
$LOAD_PATH << RUBY_LIB_LOCATION
|
|
|
|
############################################################################
|
|
# Required libraries
|
|
############################################################################
|
|
require 'erb'
|
|
require 'yaml'
|
|
require 'json'
|
|
require 'socket'
|
|
require 'webrick'
|
|
require 'pathname'
|
|
require 'optparse'
|
|
require 'opennebula'
|
|
require 'opennebula/oneflow_client'
|
|
|
|
|
|
def getServiceID(response)
|
|
rsp = JSON.parse(response)
|
|
return rsp["DOCUMENT"]["ID"]
|
|
end
|
|
|
|
def chmodService(sv, path, id, mode)
|
|
uri = "#{path}/service_template/#{id}/action"
|
|
|
|
params = {}
|
|
params["octet"] = mode
|
|
params["recursive"] = "all"
|
|
action = Service.build_json_action('chmod', params)
|
|
|
|
resp = sv.post(uri, action)
|
|
if CloudClient.is_error?(resp)
|
|
raise Exception.new("Service template chmod failed with error : #{resp}")
|
|
end
|
|
end
|
|
|
|
def getServiceTemplateByName(name, owner, sv, path)
|
|
resp = sv.get("#{path}/service_template")
|
|
if CloudClient.is_error?(resp)
|
|
raise Exception.new(resp)
|
|
return nil
|
|
else
|
|
tpls = JSON.parse(resp.body)
|
|
end
|
|
|
|
if tpls["DOCUMENT_POOL"].size != 0
|
|
tpls["DOCUMENT_POOL"]["DOCUMENT"].each do |doc|
|
|
if name == doc["NAME"] and owner == doc["UNAME"]
|
|
return doc
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
def publishService(sv, path, template, mode, owner)
|
|
tpl = JSON.parse(template)
|
|
|
|
svr = getServiceTemplateByName(tpl['name'], owner, sv, path)
|
|
if ! svr
|
|
resp = sv.post("#{path}/service_template", template)
|
|
if CloudClient.is_error?(resp)
|
|
raise Exception.new("Service template creation failed with error : #{resp}")
|
|
else
|
|
id = getServiceID(resp.body)
|
|
begin
|
|
chmodService(sv, path, id, mode)
|
|
rescue => e
|
|
raise e
|
|
end
|
|
return("created [id: #{id}]")
|
|
end
|
|
else
|
|
# Keep registration_time
|
|
if svr['TEMPLATE']['BODY'].key?("registration_time")
|
|
tpl["registration_time"] = svr['TEMPLATE']['BODY']['registration_time']
|
|
template = tpl.to_json
|
|
end
|
|
|
|
resp = sv.put("#{path}/service_template/#{svr["ID"]}", template)
|
|
if CloudClient.is_error?(resp)
|
|
raise Exception.new("Service template tupdate failed with error : #{resp}")
|
|
else
|
|
id = getServiceID(resp.body)
|
|
begin
|
|
chmodService(sv, path, id, mode)
|
|
rescue => e
|
|
raise e
|
|
end
|
|
return("updated [id: #{id}]")
|
|
end
|
|
end
|
|
return 0
|
|
end
|
|
|
|
def getTemplateByName(cli, name)
|
|
tpl_pool = OpenNebula::TemplatePool.new(cli, OpenNebula::Pool::INFO_MINE)
|
|
rc = tpl_pool.info
|
|
if OpenNebula.is_error?(rc)
|
|
puts rc.message
|
|
return nil
|
|
end
|
|
tpl_pool.each do |tpl|
|
|
if tpl.name == name
|
|
return tpl
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
def publishImage(image_name, image_comment, image_file, external_url, template, mode)
|
|
image_source = ''
|
|
root = File.expand_path(File.dirname(image_file))
|
|
filename = File.basename(File.expand_path(image_file))
|
|
|
|
# Starting a very simple HTTP server to make the image available for ONE.
|
|
http_port = nil
|
|
t1 = Thread.new do
|
|
server = WEBrick::HTTPServer.new(Port: 0,
|
|
DocumentRoot: root,
|
|
Logger: WEBrick::Log.new('/dev/null'),
|
|
AccessLog: [])
|
|
http_port = server.config[:Port]
|
|
server.start
|
|
end
|
|
|
|
# rubocop:disable Metrics/BlockLength
|
|
# Image creation and cleanup old ones
|
|
t2 = Thread.new do
|
|
begin
|
|
client = OpenNebula::Client.new(CREDENTIALS, ENDPOINT)
|
|
img_pool = OpenNebula::ImagePool.new(client, OpenNebula::Pool::INFO_MINE)
|
|
|
|
rc = img_pool.info
|
|
raise Exception, rc.message if OpenNebula.is_error?(rc)
|
|
|
|
img_pool.each do |image|
|
|
if image.name =~ /.*_tbr/
|
|
warn("Trying to delete #{image.name}")
|
|
rc = image.delete
|
|
end
|
|
next unless image.name == image_name
|
|
|
|
rc = image.delete
|
|
if OpenNebula.is_error?(rc)
|
|
rc = image.rename("#{image_name}_#{Time.now.strftime('%Y%m%d-%H%M%S')}_tbr")
|
|
raise Exception, rc.message if OpenNebula.is_error?(rc)
|
|
end
|
|
sleep(5)
|
|
end
|
|
|
|
image_source = if external_url
|
|
# We have a reverse proxy in front of us
|
|
"#{external_url}/#{HTTP_ADDR}/#{http_port}/#{filename}"
|
|
else
|
|
"http://#{HTTP_ADDR}:#{http_port}/#{filename}"
|
|
end
|
|
|
|
tmpl = if template
|
|
ERB.new(template).result(binding)
|
|
else
|
|
<<~TEMPLATE
|
|
NAME = #{image_name}
|
|
PATH = #{image_source}
|
|
TYPE = OS
|
|
PERSISTENT = No
|
|
DESCRIPTION = "#{image_comment} (default template)"
|
|
DEV_PREFIX = vd
|
|
FORMAT = qcow2
|
|
TEMPLATE
|
|
end
|
|
|
|
xml = OpenNebula::Image.build_xml
|
|
img = OpenNebula::Image.new(xml, client)
|
|
rc = img.allocate(tmpl, DS_ID)
|
|
raise Exception, rc.message if OpenNebula.is_error?(rc)
|
|
|
|
tout = 300
|
|
while img.short_state_str != 'rdy'
|
|
sleep(1)
|
|
img.info
|
|
tout -= 1
|
|
break if tout.zero?
|
|
end
|
|
img.chmod_octet(mode)
|
|
warn("\nOneNebula template publication:\n")
|
|
warn("\tImage template:\n")
|
|
warn("\t Image #{image_name} published")
|
|
warn("\t * description: #{image_comment}\n")
|
|
warn("\t * source: #{image_source}\n")
|
|
warn("\t * file: #{image_file}\n")
|
|
warn("\t * mode: #{mode}\n")
|
|
rescue Exception => e
|
|
warn(e.message)
|
|
Thread.kill(t1)
|
|
exit(-1)
|
|
end
|
|
Thread.kill(t1)
|
|
end
|
|
# rubocop:enable Metrics/BlockLength
|
|
|
|
t1.join
|
|
t2.join
|
|
end
|
|
|
|
def publishVM(oneCli, template_name, template, mode)
|
|
xml = OpenNebula::Template.build_xml
|
|
tpl = nil
|
|
|
|
rc = nil
|
|
print("\tVM template #{template_name} :",)
|
|
tpl = getTemplateByName(oneCli, template_name)
|
|
if tpl
|
|
rc = tpl.update(template)
|
|
print(" update ")
|
|
else
|
|
tpl = OpenNebula::Template.new(xml, oneCli)
|
|
rc = tpl.allocate(template)
|
|
print(" create ")
|
|
end
|
|
|
|
if OpenNebula.is_error?(rc)
|
|
puts("[KO]")
|
|
STDERR.puts rc.message
|
|
exit(-1)
|
|
end
|
|
print("\n\tSet VM template #{template_name} permission to #{mode}")
|
|
tpl.chmod_octet(mode)
|
|
puts ("[OK]")
|
|
return 0
|
|
end
|
|
|
|
|
|
options = {}
|
|
|
|
OptionParser.new do |opts|
|
|
opts.banner = "Usage: onte-templates [options]"
|
|
|
|
opts.on("-cFILE", "--config=FILE", "Configuration file to use (default ./.one-templates.conf)") do |c|
|
|
options[:config_file] = c
|
|
end
|
|
|
|
opts.on("-tTYPE", "--type=TYPE", "Set what do you want to publish (vm for a vm_template, service for a service_template)") do |t|
|
|
options[:type] = t
|
|
end
|
|
|
|
opts.on("-nNAME", "--name=NAME", "Name of the template to publish") do |n|
|
|
options[:name] = n
|
|
end
|
|
|
|
opts.on("-TTEMPLATE", "--template=TEMPLATE", "The template to publish (file or raw template)") do |tp|
|
|
options[:template] = tp
|
|
end
|
|
|
|
opts.on("-dDIRECTORY", "--directory=DIRECTORY", "Template directory") do |d|
|
|
options[:directory] = d
|
|
end
|
|
|
|
opts.on("-uUSER", "--user=USER", "OpenNebula user") do |u|
|
|
options[:user] = u
|
|
end
|
|
|
|
opts.on("-pTOKEN", "--password=TOKEN", "OpenNebula user token or password") do |t|
|
|
options[:token] = t
|
|
end
|
|
|
|
opts.on("-eENDPOINT", "--end-point=ENDPOINT", "OpenNebula cluster API end point") do |e|
|
|
options[:endpoint] = e
|
|
end
|
|
|
|
opts.on("-fFLOWENDPOINT", "--flow-end-point=FLOWENDPOINT", "OneFlow API end point") do |f|
|
|
options[:flow_endpoint] = f
|
|
end
|
|
|
|
opts.on("-mMODE", "--mode=MODE", "Permissions for the template (ex: 644)") do |m|
|
|
options[:mode] = m
|
|
end
|
|
|
|
opts.on("-bBUILDER_ADDR","--builder-addr=BUILDER_ADDR", "Builder IP address") do |b|
|
|
options[:builder_addr] = b
|
|
end
|
|
|
|
opts.on("-xEXTERNAL", "--external-url=EXTERNAL", "External URL (reverse proxy)") do |x|
|
|
options[:external_url] = x
|
|
end
|
|
|
|
opts.on("-sDATASTORE_ID", "--datasore-id=DATASTORE_ID", "Images datastore ID") do |s|
|
|
options[:datastore_id] = s
|
|
end
|
|
|
|
opts.on("-iIMAGE_ROOT", "--image-root=IMAGE_ROOT", "Directory containing the images") do |i|
|
|
options[:image_root] = i
|
|
end
|
|
|
|
opts.on("-cCOMMENT", "--comment=COMMENT", "Image comment/description") do |c|
|
|
options[:image_comment] = c
|
|
end
|
|
|
|
opts.on("-IIMAGE", "--image-file=IMAGE", "Image file do publish") do |img|
|
|
options[:image_file] = img
|
|
end
|
|
|
|
opts.on("-VIMAGE_NAME", "--image-name=IMAGE_NAME", "Image name for vm template") do |img|
|
|
options[:image_name] = img
|
|
end
|
|
|
|
opts.on("-vVM_NAME", "--vm-name=IMAGE_NAME", "VM Template name") do |vm|
|
|
options[:vm_name] = vm
|
|
end
|
|
|
|
opts.on("-h", "--help", "Prints this help") do
|
|
puts opts
|
|
exit
|
|
end
|
|
end.parse!
|
|
|
|
config_file = if ENV.has_key?("TEMPLATER_CONFIG")
|
|
ENV["TEMPLATER_CONFIG"]
|
|
elsif options.key?(:config_file)
|
|
options[:config_file]
|
|
else
|
|
"#{File.dirname(__FILE__)}/.one-templates.conf"
|
|
end
|
|
|
|
config = if File.readable?(config_file)
|
|
YAML.load_file(config_file)
|
|
else
|
|
{}
|
|
end
|
|
|
|
# OpenNebula credentials
|
|
user = ""
|
|
token = ""
|
|
|
|
if options.key?(:user) and options.key?(:token)
|
|
user = options[:user]
|
|
token = options[:token]
|
|
elsif ENV.has_key?("ONE_USER") and ENV.has_key?("ONE_TOKEN")
|
|
user = ENV["ONE_USER"]
|
|
token = ENV["ONE_TOKEN"]
|
|
elsif config.key?("user") and config.key?("token")
|
|
user = config["user"]
|
|
token = config["token"]
|
|
elsif File.file?("~/.one/one_auth")
|
|
creds = File.read("~/.one/one_auth").chomp.split(':')
|
|
user = creds[0]
|
|
token = creds[1]
|
|
else
|
|
raise Exception.new("OpenNebula user or token or both are missing, provide this informations in configuration or in environement")
|
|
end
|
|
|
|
template_type = if options.key?(:type)
|
|
options[:type]
|
|
elsif ENV.has_key?("TEMPLATE_TYPE")
|
|
ENV["TEMPLATE_TYPE"]
|
|
else
|
|
raise Exception.new("Publishing type is not defined, use --type or TYPE environement variable.")
|
|
end
|
|
if (template_type != "service") && (template_type != "vm") && (template_type != 'image')
|
|
raise Exception.new("Type #{template_type} not supported. Type has to be 'image', 'vm' or 'service'")
|
|
end
|
|
|
|
template_dir = ""
|
|
if options.key?(:directory)
|
|
template_dir = options[:directory]
|
|
elsif ENV.has_key?("SERVICE_TEMPLATE_DIR")
|
|
template_dir = ENV["SERVICE_TEMPLATE_DIR"]
|
|
elsif config.key?("template_dir")
|
|
template_dir = config[:template_dir]
|
|
else
|
|
if template_type == "service"
|
|
template_dir = "#{File.dirname(__FILE__)}/../templates/one/service_template"
|
|
elsif template_type == "vm"
|
|
template_dir = "#{File.dirname(__FILE__)}/../templates/one/vm"
|
|
elsif template_type == "image"
|
|
template_dir = "#{File.dirname(__FILE__)}/../templates/one/image"
|
|
end
|
|
end
|
|
|
|
template = if options.key?(:template)
|
|
if File.readable?(options[:template])
|
|
File.read(options[:template])
|
|
else
|
|
options[:template]
|
|
end
|
|
elsif ENV.has_key?("TEMPLATE")
|
|
ENV("TEMPLATE")
|
|
else
|
|
nil
|
|
end
|
|
|
|
template_name = if options[:name]
|
|
options[:name]
|
|
elsif ENV.has_key?("TEMPLATE_NAME")
|
|
ENV["TEMPLATE_NAME"]
|
|
end
|
|
|
|
template_file = nil
|
|
|
|
tplExt = "json"
|
|
if template_type == "vm"
|
|
tplExt = "xml"
|
|
elsif template_type == "image"
|
|
tplExt = "tpl"
|
|
end
|
|
|
|
|
|
# XML_RPC endpoint where OpenNebula is listening
|
|
end_point = nil
|
|
if options[:endpoint]
|
|
end_point = options[:endpoint]
|
|
elsif ENV.has_key?("ONE_XMLRPC")
|
|
end_point = ENV["ONE_XMLRPC"]
|
|
elsif config.key?("endpoint")
|
|
end_point = config["endpoint"]
|
|
end
|
|
|
|
flow_endpoint = nil
|
|
if template_type == "service"
|
|
if options[:flow_endpoint]
|
|
flow_end_point = URI.parse(options[:flow_endpoint])
|
|
elsif ENV.has_key?("ONE_FLOW_ENDPOINT")
|
|
flow_end_point = URI.parse(ENV["ONE_FLOW_ENDPOINT"])
|
|
elsif config.key?("flow_endpoint")
|
|
flow_end_point = URI.parse(config["flow_endpoint"])
|
|
end
|
|
if ! flow_end_point
|
|
raise Exception.new("OneFlow API endpoint is missing, use --flow-end-point option or ONE_FLOW_ENDPOINT environement variable")
|
|
end
|
|
|
|
flow_path = flow_end_point.path
|
|
end
|
|
|
|
if ! end_point
|
|
raise Exception.new("API endpoint is missing, use --end-point option or ONE_XMLRPC environement variable")
|
|
end
|
|
|
|
|
|
mode = nil
|
|
if options[:mode]
|
|
mode = options[:mode]
|
|
elsif ENV.has_key?("MODE")
|
|
mode = ENV["MODE"]
|
|
else
|
|
mode = "600"
|
|
end
|
|
|
|
external_url = if options[:external_url]
|
|
options[:external_url]
|
|
elsif ENV.key?('EXTERNAL_URL')
|
|
ENV['EXTERNAL_URL']
|
|
elsif config.key?("external_url")
|
|
config["external_url"]
|
|
end
|
|
|
|
builder_addr = if options[:builder_addr]
|
|
options[:buider_addr]
|
|
elsif ENV.key?('BUILDER_ADDR')
|
|
ENV['BUILDER_ADDR']
|
|
elsif config.key?("builder_addr")
|
|
config["builder_addr"]
|
|
else
|
|
# Get first IP address
|
|
Socket.getifaddrs.detect do |addr_info|
|
|
addr_info.name != 'lo' && addr_info.addr && addr_info.addr.ipv4?
|
|
end.addr.ip_address
|
|
end
|
|
|
|
datastore_id = if options[:datastore_id]
|
|
options[:datastore_id]
|
|
elsif ENV.key?('DATASTORE_ID')
|
|
ENV['DATASTORE_ID'].to_i
|
|
elsif config.key?("datastore_id")
|
|
config["datastore_id"].to_i
|
|
else
|
|
1
|
|
end
|
|
|
|
image_root = if options[:image_root]
|
|
options[:image_root]
|
|
elsif ENV.key?('IMAGE_ROOT')
|
|
ENV['IMAGE_ROOT']
|
|
elsif config[:image_root]
|
|
config['image_root']
|
|
else
|
|
"#{File.dirname(__FILE__)}/../output"
|
|
end
|
|
|
|
image_comment = if options[:image_comment]
|
|
options[:image_comment]
|
|
elsif ENV.key?('IMAGE_COMMENT')
|
|
ENV['IMAGE_COMMENT']
|
|
elsif config[:image_comment]
|
|
config['image_comment']
|
|
else
|
|
"#{template_name}"
|
|
end
|
|
|
|
image_file = if options[:image_file]
|
|
options[:image_file]
|
|
elsif ENV.key?('IMAGE_FILE')
|
|
ENV['IMAGE_FILE']
|
|
elsif config.key?(:image_file)
|
|
config['image_file']
|
|
else
|
|
nil
|
|
end
|
|
|
|
image_name = if options[:image_name]
|
|
options[:image_name]
|
|
elsif ENV.key?('IMAGE_NAME')
|
|
ENV['IMAGE_NAME']
|
|
elsif config.key?(:image_name)
|
|
config[:image_name]
|
|
else
|
|
nil
|
|
end
|
|
|
|
vm_name = if options[:vm_name]
|
|
options[:vm_name]
|
|
elsif ENV.key?('VM_NAME')
|
|
ENV['VM_NAME']
|
|
elsif config.key?(:vm_name)
|
|
config[:vm_name]
|
|
else
|
|
nil
|
|
end
|
|
|
|
CREDENTIALS = "#{user}:#{token}"
|
|
ENDPOINT = end_point
|
|
DS_ID = datastore_id
|
|
HTTP_ADDR = builder_addr
|
|
|
|
oneCli = OpenNebula::Client.new(CREDENTIALS, ENDPOINT)
|
|
|
|
# Template management
|
|
# the template can be an ERB template
|
|
# if you provide a template we use it as raw template
|
|
# if you provide a file name we read it first
|
|
#
|
|
|
|
tpl_content = nil
|
|
if template
|
|
if File.readable?(template)
|
|
tpl_content = File.read(template)
|
|
else
|
|
tpl_content = template
|
|
end
|
|
else
|
|
if template_name
|
|
fname = "#{template_dir}/#{template_name}.#{tplExt}"
|
|
if File.readable?(fname)
|
|
tpl_content = File.read(fname)
|
|
elsif template_type != "image"
|
|
raise Exception.new("No service or vm named #{template_name}, file #{fname} is missing !")
|
|
end
|
|
else
|
|
raise Exception.new("No template provided, template name is missing, please provide a service name with option --name")
|
|
end
|
|
end
|
|
|
|
# Process the ERB template.
|
|
# For the images the template is processed later during publishing
|
|
if template_type != "image"
|
|
tpl = if File.readable?(tpl_content)
|
|
ERB.new(File.read(tpl_content))
|
|
else
|
|
ERB.new(tpl_content)
|
|
end
|
|
template = tpl.result(binding)
|
|
end
|
|
|
|
if template_type == "service"
|
|
sv = Service::Client.new(
|
|
:username => user,
|
|
:password => token,
|
|
:url => flow_end_point.to_s,
|
|
:user_agent => 'CLI')
|
|
begin
|
|
puts("OpenNebula template publication:")
|
|
res = publishService(sv, flow_path, template, mode, user)
|
|
puts("\tService template #{template_name} #{res}")
|
|
rescue => err
|
|
puts(err)
|
|
end
|
|
elsif template_type == "vm"
|
|
begin
|
|
puts("OpenNebula template publication:")
|
|
publishVM(oneCli, template_name, template, mode)
|
|
rescue => err
|
|
puts(err)
|
|
end
|
|
|
|
elsif template_type == "image"
|
|
if ! image_file
|
|
raise Exception.new("No image file provided, use --image-file option or IMAGE_FILE environement variable.")
|
|
exit(-1)
|
|
end
|
|
publishImage(template_name, image_comment, image_file, external_url, template, mode)
|
|
end
|