pve/lib/pve/proxmox.rb

598 lines
15 KiB
Ruby

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 <<self
def __new__ data
n = allocate
n.send :__update__, data
end
private :__new__
end
def respond_to? method
super or instance_variable_defined?( "@#{method}")
end
def method_missing method, *args, &exe
if instance_variable_defined? "@#{method}"
instance_variable_get "@#{method}"
else
super
end
end
def __update__ data
instance_variables.each do |k|
remove_instance_variable k
end
data.each do |k,v|
instance_variable_set "@#{k}", v
end
initialize
self
end
def refresh!
__update__ rest_get( @rest_prefix)
end
end
class Node < Base
class <<self
def find_by_name name
all.each do |node|
return node if node.node == name
end
nil
end
def find_by_name! name
find_by_name( name) or raise( Proxmox::NotFound, "Node not found: #{name}")
end
def all
rest_get( '/nodes').map {|d| __new__ d.merge( t: 'nd') }
end
end
attr_reader :name
def === t
@name =~ t or @vmid.to_s =~ t or @sid =~ t
end
def initialize
@rest_prefix = "/nodes/#{@node}"
@sid = "nd:#{@node}"
@name = @node
end
def offline?() @status.nil? or 'offline' == @status end
def online?() 'online' == @status end
def lxc
return [] if offline?
rest_get( "#{@rest_prefix}/lxc").map {|d| LXC.send :__new__, d.merge( node: self, t: 'ct') }
end
def qemu
return [] if offline?
rest_get( "#{@rest_prefix}/qemu").map {|d| Qemu.send :__new__, d.merge( node: self, t: 'qm') }
end
def tasks
return [] if offline?
rest_get( "/#{@rest_prefix}/tasks").map {|d| Task.send :__new__, d.merge( node: self, t: 'task') }
end
def enter *command
Kernel.system 'ssh', '-t', node, command.map( &:to_s).shelljoin
end
def exec *command
Kernel.system 'ssh', node, command.map( &:to_s).shelljoin
end
end
class Task < Base
def initialize
@rest_prefix = "/nodes/#{@node.node}/tasks/#{upid}"
@sid = upid
end
def inspect
"#<#{self.class.name} #{upid}>"
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 <<self
def all
Node.all.flat_map {|n| n.qemu }
end
def find_by_vmid vmid
vmid = vmid.to_s
Node.all.each do |n|
n.qemu.each do |l|
return l if l.vmid == vmid
end
end
nil
end
def find_by_name name
Node.all.each do |n|
n.qemu.each do |l|
return l if l.name == name
end
end
nil
end
def find name_or_id = nil, name: nil, vmid: nil
if (name and vmid) or (name and name_or_id) or (name_or_id and vmid)
raise Proxmox::NotFound, "name xor vmid needed to find CT, not both."
elsif name
find_by_name name
elsif vmid
find_by_vmid vmid.to_i
elsif name_or_id =~ /\D/
find_by_name name_or_id
elsif name_or_id.is_a?( Numeric) or name_or_id =~ /\d/
find_by_vmid name_or_id.to_i
else
raise Proxmox::NotFound, "name xor vmid needed to find CT."
end
end
def find_by_vmid! name
find_by_vmid( name) or raise( Proxmox::NotFound, "Virtual Machine not found: #{name}")
end
def find_by_name! name
find_by_name( name) or raise( Proxmox::NotFound, "Virtual Machine not found: #{name}")
end
def find! name
find( name) or raise( Proxmox::NotFound, "Virtual Machine not found: #{name}")
end
end
def initialize
@rest_prefix = "/nodes/%s/qemu/%d" % [@node.node, @vmid]
@sid = "qm:#{@vmid}"
end
def ha
HA.find self
end
def exec *args
node.exec 'qm', 'guest', 'exec', vmid, '--', *args
end
end
class LXC < Hosted
class <<self
def all
Node.all.flat_map {|n| n.lxc }
end
def find_by_vmid vmid
vmid = vmid.to_s
Node.all.each do |n|
n.lxc.each do |l|
return l if l.vmid == vmid
end
end
nil
end
def find_by_name name
Node.all.each do |n|
n.lxc.each do |l|
return l if l.name == name
end
end
nil
end
def find name_or_id = nil, name: nil, vmid: nil
if (name and vmid) or (name and name_or_id) or (name_or_id and vmid)
raise ArgumentError, "name xor vmid needed to find CT, not both."
elsif name
find_by_name name
elsif vmid
find_by_vmid vmid.to_i
elsif name_or_id =~ /\D/
find_by_name name_or_id
elsif name_or_id.is_a?( Numeric) or name_or_id =~ /\d/
find_by_vmid name_or_id.to_i
else
raise ArgumentError, "name xor vmid needed to find CT."
end
end
def find_by_vmid! name
find_by_vmid( name) or raise( Proxmox::NotFound, "Container not found: #{name}")
end
def find_by_name! name
find_by_name( name) or raise( Proxmox::NotFound, "Container not found: #{name}")
end
def find! name
find( name) or raise( Proxmox::NotFound, "Container not found: #{name}")
end
def create template, **options
tmplt = PVE::CTTemplate.const_get( template&.classify || 'Default').new **options
name = tmplt.name
virts = Proxmox::LXC.all + Proxmox::Qemu.all
vmid = tmplt.vmid
if virt = virts.find {|v| v.vmid == vmid }
raise Proxmox::AlreadyExists, "VT/VM with vmid [#{vmid}] already exists: #{virt.name}"
end
already_exists = virts.select {|v| v.name == name }
unless already_exists.empty?
raise Proxmox::AlreadyExists, "CT/VM named [#{name}] already exists: vmid: #{already_exists.map( &:vmid).inspect}"
end
node = Proxmox::Node.find_by_name! tmplt.node
options = {
ostemplate: tmplt.ostemplate,
vmid: vmid.to_s,
arch: tmplt.arch,
cmode: tmplt.cmode,
cores: tmplt.cores.to_i,
description: tmplt.description,
hostname: tmplt.hostname,
memory: tmplt.memory,
net0: tmplt.net0&.map {|k,v| "#{k}=#{v}" }&.join(','),
net1: tmplt.net1&.map {|k,v| "#{k}=#{v}" }&.join(','),
net2: tmplt.net2&.map {|k,v| "#{k}=#{v}" }&.join(','),
net3: tmplt.net3&.map {|k,v| "#{k}=#{v}" }&.join(','),
ostype: tmplt.ostype,
:'ssh-public-keys' => 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 <<self
def find host
ha = HA.send :__new__, digest: nil, state: nil, virt: host
ha.refresh!
end
def create host, **options
find( host).create **options
end
end
def refresh!
virt = @virt
__update__( {state: nil}.merge( rest_get( "/cluster/ha/resources/#{virt.sid}")).merge( virt: virt))
self
rescue RestClient::InternalServerError
__update__ digest: nil, state: nil, virt: virt
end
def create group: nil, comment: nil, max_relocate: nil, max_restart: nil, state: nil
options = {
sid: virt.sid,
group: group,
comment: comment,
max_relocate: max_relocate,
max_restart: max_restart,
state: state
}
options.delete_if {|k,v| v.nil? }
rest_post "/cluster/ha/resources", **options
refresh!
end
def delete
rest_del "#{@rest_prefix}"
end
def state= state
rest_put "#{@rest_prefix}", state: state.to_s
refresh!
end
def started!
self.state = :started
self
end
def stopped!
self.state = :stopped
self
end
def disabled!
self.state = :disabled
self
end
def started?
'started' == self.state
end
def stopped?
'stopped' == self.state
end
def error?
'error' == self.state
end
def active?
! ! digest
end
end
end
require_relative 'templates'