require 'rest_client' require 'cgi' require 'json' require 'ipaddress' require 'shellwords' require 'active_support/all' module Proxmox class Exception < ::Exception end class NotFound < Exception end class AlreadyExists < Exception end def self.connect username, password, realm: nil, verify_tls: nil, uri: nil uri ||= 'https://localhost:8006/api2/json' cred = { username: username, password: password, realm: realm || 'pve' } @@connection = RestClient::Resource.new( uri, verify_ssl: ! ! verify_tls).tap do |rs| resp = rs['/access/ticket'].post cred case resp.code when 200 data = JSON.parse resp.body, symbolize_names: true @@api_ticket = { CSRFPreventionToken: data[:data][:CSRFPreventionToken], cookie: "PVEAuthCookie=#{CGI.escape data[:data][:ticket]}" } else raise Exception, "Authentication failed" end end end def self.connection @@connection end def self.api_ticket @@api_ticket end def self.find_by_name name Proxmox::LXC.find_by_name( name) || Proxmox::Qemu.find_by_name( name) || Proxmox::Node.find_by_name( name) end def self.find_by_vmid vmid Proxmox::LXC.find_by_vmid( vmid) || Proxmox::Qemu.find_by_vmid( vmid) || Proxmox::Node.find_by_vmid( vmid) end def self.find name_or_id Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find( name_or_id) || Proxmox::Node.find( name_or_id) end module RestConnection def __response__ resp case resp.code when 200 JSON.parse( resp.body, symbolize_names: true)[:data] when 500 nil else raise "Request failed of #{req.url} [#{resp.code}]: #{resp.body.inspect}" end end def __headers__ **hdrs Proxmox.api_ticket.merge( 'Accept' => 'application/json').merge( hdrs) end def __data__ **data case data when String data else data.to_json end end private :__response__, :__headers__, :__data__ def bench path #t = Time.now r = yield #p path => Time.now-t r end def rest_get path, **data, &exe data = data.delete_if {|k,v|v.nil?} path += "#{path.include?( ??) ? ?& : ??}#{data.map{|k,v|"#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"}.join '&'}" unless data.empty? __response__ Proxmox.connection[path].get( __headers__( :'Content-Type' => 'application/json')) end def rest_put path, **data, &exe __response__ Proxmox.connection[path].put( __data__( data), __headers__( :'Content-Type' => 'application/json')) end def rest_del path, &exe __response__ Proxmox.connection[path].delete( __headers__( :'Content-Type' => 'application/json')) end def rest_post path, **data, &exe __response__ Proxmox.connection[path].post( __data__( data), __headers__( :'Content-Type' => 'application/json')) end end class Base include RestConnection extend RestConnection attr_reader :sid class <" end #def finished? # rest_get( "/nodes/#{node}/tasks/") #end def status rest_get( "#{@rest_prefix}/status") end def log start: nil, limit: nil rest_get( "#{@rest_prefix}/log", start: start, limit: limit) end end class Hosted < Base def refresh! node, t = @node, @t __update__ rest_get( "#{@rest_prefix}/status/current").merge( node: node, t: t) end def === t @name =~ t or @vmid.to_s =~ t or @sid =~ t end def migrate node node = case node when Node then node else Node.find!( node.to_s) end Task.send :__new__, node: @node, host: self, upid: rest_post( "#{@rest_prefix}/migrate", target: node.node) end def start Task.send :__new__, node: @node, host: self, upid: rest_post( "#{@rest_prefix}/status/start") end def stop Task.send :__new__, node: @node, host: self, upid: rest_post( "#{@rest_prefix}/status/stop") end def destroy Task.send :__new__, node: @node, host: self, upid: rest_del( "#{@rest_prefix}") end def current_status rest_get "#{@rest_prefix}/status/current" end def running? current_status[:status] == 'running' end def stopped? current_status[:status] == 'stopped' end def wait forstatus, timeout: nil, secs: nil, lock: nil, &exe forstatus = forstatus.to_s secs ||= 0.2 b = Time.now + timeout.to_i + secs loop do d = current_status return true if d[:status] == forstatus and (not lock or lock == d[:lock]) exe.call d[:status], d[:lock] if exe return false if timeout and b <= Time.now sleep secs end nil end def config cnf = rest_get "#{@rest_prefix}/config" cnf[:network] = cnf. keys. map( &:to_s). grep( /\Anet\d+\z/). map do |k| nc = {card: k} cnf.delete( k.to_sym). split( ','). each do |f| k, v = f.split( '=', 2) nc[k.to_sym] = v end nc[:ip] &&= IPAddress::IPv4.new nc[:ip] nc[:gw] &&= IPAddress::IPv4.new nc[:gw] nc[:mtu] &&= nc[:mtu].to_i nc[:tag] &&= nc[:tag].to_i nc[:firewall] &&= 1 == nc[:firewall].to_i nc end cnf[:unprivileged] &&= 1 == cnf[:unprivileged] cnf[:memory] &&= cnf[:memory].to_i cnf[:cores] &&= cnf[:cores].to_i cnf end def cnfset **cnf r = {delete: []} cnf.each do |k,v| case v when true then r[k] = 1 when false then r[k] = 0 when nil then r[:delete].push k else r[k] = v end end r.delete :delete if r[:delete].empty? rest_put "#{@rest_prefix}/config", r end end class Qemu < Hosted class < tmplt.ssh_public_keys, storage: tmplt.storage, #rootfs: { # volume: "root:vm-#{vmid}-disk-0", size: '4G' #}.map {|k,v| "#{k}=#{v}" }.join( ','), swap: tmplt.swap, unprivileged: tmplt.unprivileged, }.delete_if {|k,v| v.nil? } @temp = LXC.send :__new__, node: node, vmid: options[:vmid], name: name, hostname: options[:hostname] upid = rest_post( "/nodes/%s/lxc" % node.node, **options) Task.send :__new__, node: node, host: @temp, upid: upid end end def initialize @rest_prefix = "/nodes/%s/lxc/%d" % [@node.node, @vmid] @sid = "ct:#{@vmid}" end def ha HA.find self end def exec *args node.exec 'pct', 'exec', vmid, '--', *args end def enter node.enter 'pct', 'enter', vmid end end class HA < Base def initialize @rest_prefix = "/cluster/ha/resources/#{virt.sid}" end class <