#!/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