vms/tools/one-templates
2023-06-29 16:38:20 +02:00

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