pve 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +70 -0
- data/README.adoc +22 -0
- data/bin/pvecli +5 -0
- data/lib/pve.rb +4 -0
- data/lib/pve/cli.rb +159 -0
- data/lib/pve/cli/base.rb +187 -0
- data/lib/pve/cli/ct.rb +115 -0
- data/lib/pve/cli/ha.rb +78 -0
- data/lib/pve/cli/node.rb +46 -0
- data/lib/pve/cli/qm.rb +32 -0
- data/lib/pve/cli/task.rb +26 -0
- data/lib/pve/helper.rb +167 -0
- data/lib/pve/proxmox.rb +597 -0
- data/lib/pve/qm.rb +32 -0
- data/lib/pve/templates.rb +154 -0
- data/lib/pve/version.rb +3 -0
- data/pve.gemspec +37 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e2f41d6fba5690429b625a55d147bf468834c1e705d6e73fdcd879566037dd47
|
4
|
+
data.tar.gz: 5237da733be939f52f26818f6c12a20d2389c5a26e8ba7343cbce62bcb280001
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: adfe44900fc74f93932b12ffed27abced7d33e04c910fad519b406755d8d9f9de140347341da45a3a35d4e315f4c5e7b5286af2ec4fbdf6ec2f9116a94221224
|
7
|
+
data.tar.gz: 3b55ec2a97b1973ae66d08e17cb50c70ec765305292748fe306bb99e3dd30bf33a46c5bc824570a8786e832c767f422e4da814ad14dd0497ab83ec707d46e0e9
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pve (0.1.1)
|
5
|
+
activesupport (>= 2)
|
6
|
+
dencli (~> 0.3.1)
|
7
|
+
ipaddress (~> 0.8.3)
|
8
|
+
pmap (~> 1.1)
|
9
|
+
rest-client (~> 2.1)
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: https://rubygems.org/
|
13
|
+
specs:
|
14
|
+
activesupport (6.1.3.1)
|
15
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
16
|
+
i18n (>= 1.6, < 2)
|
17
|
+
minitest (>= 5.1)
|
18
|
+
tzinfo (~> 2.0)
|
19
|
+
zeitwerk (~> 2.3)
|
20
|
+
concurrent-ruby (1.1.8)
|
21
|
+
dencli (0.3.1)
|
22
|
+
diff-lcs (1.4.4)
|
23
|
+
domain_name (0.5.20190701)
|
24
|
+
unf (>= 0.0.5, < 1.0.0)
|
25
|
+
http-accept (1.7.0)
|
26
|
+
http-cookie (1.0.3)
|
27
|
+
domain_name (~> 0.5)
|
28
|
+
i18n (1.8.10)
|
29
|
+
concurrent-ruby (~> 1.0)
|
30
|
+
ipaddress (0.8.3)
|
31
|
+
mime-types (3.3.1)
|
32
|
+
mime-types-data (~> 3.2015)
|
33
|
+
mime-types-data (3.2021.0225)
|
34
|
+
minitest (5.14.4)
|
35
|
+
netrc (0.11.0)
|
36
|
+
pmap (1.1.1)
|
37
|
+
rest-client (2.1.0)
|
38
|
+
http-accept (>= 1.7.0, < 2.0)
|
39
|
+
http-cookie (>= 1.0.2, < 2.0)
|
40
|
+
mime-types (>= 1.16, < 4.0)
|
41
|
+
netrc (~> 0.8)
|
42
|
+
rspec (3.10.0)
|
43
|
+
rspec-core (~> 3.10.0)
|
44
|
+
rspec-expectations (~> 3.10.0)
|
45
|
+
rspec-mocks (~> 3.10.0)
|
46
|
+
rspec-core (3.10.1)
|
47
|
+
rspec-support (~> 3.10.0)
|
48
|
+
rspec-expectations (3.10.1)
|
49
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
50
|
+
rspec-support (~> 3.10.0)
|
51
|
+
rspec-mocks (3.10.2)
|
52
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
53
|
+
rspec-support (~> 3.10.0)
|
54
|
+
rspec-support (3.10.2)
|
55
|
+
tzinfo (2.0.4)
|
56
|
+
concurrent-ruby (~> 1.0)
|
57
|
+
unf (0.1.4)
|
58
|
+
unf_ext
|
59
|
+
unf_ext (0.0.7.7)
|
60
|
+
zeitwerk (2.4.2)
|
61
|
+
|
62
|
+
PLATFORMS
|
63
|
+
x86_64-linux
|
64
|
+
|
65
|
+
DEPENDENCIES
|
66
|
+
pve!
|
67
|
+
rspec (~> 3.2)
|
68
|
+
|
69
|
+
BUNDLED WITH
|
70
|
+
2.2.15
|
data/README.adoc
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Proxmox Virtual Environment High Level API for Ruby
|
2
|
+
===================================================
|
3
|
+
|
4
|
+
This is a limited, but easier to use library for ruby.
|
5
|
+
It provides additional a command line interface for administration named `pvecli`.
|
6
|
+
The Rest-API will be used for controlling your server.
|
7
|
+
|
8
|
+
You need to provide a config-file `/etc/pve/pvecli.yml`:
|
9
|
+
|
10
|
+
auth:
|
11
|
+
username: USERNAME
|
12
|
+
password: PASSWORD
|
13
|
+
realm: pve or something like that
|
14
|
+
connect:
|
15
|
+
verify_tls: no if you do not use known CA-signed X509-Certificates
|
16
|
+
|
17
|
+
pvecli
|
18
|
+
======
|
19
|
+
|
20
|
+
This tool should usable like PVE-WebGUI, instead of low-level-tools like `pct`
|
21
|
+
or user-unfriendlier tools like `pvesh`.
|
22
|
+
So `pvecli` provides a global control over your cluster on command line.
|
data/bin/pvecli
ADDED
data/lib/pve.rb
ADDED
data/lib/pve/cli.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'dencli'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'pve'
|
5
|
+
require_relative 'helper'
|
6
|
+
require_relative 'cli/base'
|
7
|
+
require_relative 'cli/ct'
|
8
|
+
require_relative 'cli/ha'
|
9
|
+
require_relative 'cli/task'
|
10
|
+
require_relative 'cli/qm'
|
11
|
+
require_relative 'cli/node'
|
12
|
+
|
13
|
+
class UsageError <RuntimeError
|
14
|
+
end
|
15
|
+
|
16
|
+
class PVE::Cli
|
17
|
+
attr_reader :cfg, :cli
|
18
|
+
|
19
|
+
def connect
|
20
|
+
@conn ||=
|
21
|
+
Proxmox.connect cfg[:auth][:username], cfg[:auth][:password],
|
22
|
+
realm: cfg[:auth][:realm], **cfg[:connect]
|
23
|
+
#RestClient.log = STDERR
|
24
|
+
@conn
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize config_file = nil
|
28
|
+
config_file ||= '/etc/pve/pvecli.yml'
|
29
|
+
@cfg =
|
30
|
+
YAML.safe_load File.read( config_file), [], [], false,
|
31
|
+
config_file, symbolize_names: true
|
32
|
+
@cli = DenCli.new File.basename($0), "PVE CLI"
|
33
|
+
@interactive = false
|
34
|
+
prepare
|
35
|
+
end
|
36
|
+
|
37
|
+
def interactive?
|
38
|
+
@interactive
|
39
|
+
end
|
40
|
+
|
41
|
+
def enter host, *args
|
42
|
+
r = host.enter *args
|
43
|
+
STDERR.puts "! #{$?.exitstatus}" unless r
|
44
|
+
end
|
45
|
+
|
46
|
+
def wait_state host, state, timeout: nil, lock: nil
|
47
|
+
spinners = %w[- / | \\]
|
48
|
+
spin = 0
|
49
|
+
r =
|
50
|
+
host.wait state, lock: lock, timeout: timeout, secs: 0.2 do |state, lock|
|
51
|
+
lock = " (locked: #{lock})" if lock
|
52
|
+
STDERR.printf "\r[%s] %s Still %s%s...\e[J\n", host.name, spinners[spin = (spin + 1) % 4], state, lock
|
53
|
+
end
|
54
|
+
STDERR.printf "\r\e[J"
|
55
|
+
exit 1 unless interactive? and r
|
56
|
+
end
|
57
|
+
|
58
|
+
def wait task, secs: nil, text: nil
|
59
|
+
secs ||= 0.1
|
60
|
+
spinners, spin, logn = "▖▘▝▗", 0, 0
|
61
|
+
STDERR.puts task.upid
|
62
|
+
loop do
|
63
|
+
s = task.status
|
64
|
+
if s[:exitstatus]
|
65
|
+
STDERR.printf "\r[%s] %s %s\e[J\n",
|
66
|
+
task.host ? task.host.name : s[:id],
|
67
|
+
case s[:exitstatus]
|
68
|
+
when "OK" then "\e[32;1m✓\e[0m"
|
69
|
+
else "\e[31;1m✗\e[0m"
|
70
|
+
end,
|
71
|
+
s[:status]
|
72
|
+
return task
|
73
|
+
end
|
74
|
+
log = task.log start: logn
|
75
|
+
log = [] if [{n: 1, t: 'no content'}] == log
|
76
|
+
unless log.empty?
|
77
|
+
STDERR.printf "\r\e[J"
|
78
|
+
log.each {|l| puts l[:t] }
|
79
|
+
logn = log.last[:n]
|
80
|
+
end
|
81
|
+
STDERR.printf "\r[%s] \e[33;1m%s\e[0m %s...\e[J", task.host ? task.host.name : s[:id], spinners[spin = (spin + 1) % 4], text || "Working"
|
82
|
+
sleep secs
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def start host, node: nil, timeout: nil, fire: nil, secs: nil
|
87
|
+
timeout ||= 30
|
88
|
+
if node
|
89
|
+
task = host.migrate Proxmox::Node.find_by_name!( node)
|
90
|
+
wait task, text: "Migrating"
|
91
|
+
end
|
92
|
+
if host.running?
|
93
|
+
STDERR.puts "Already running."
|
94
|
+
return
|
95
|
+
end
|
96
|
+
task = host.start
|
97
|
+
unless fire
|
98
|
+
wait task, text: "Starting"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def stop host, timeout: nil, fire: nil, secs: nil
|
103
|
+
timeout ||= 30
|
104
|
+
if host.stopped?
|
105
|
+
STDERR.puts "Already stopped."
|
106
|
+
return
|
107
|
+
end
|
108
|
+
task = host.stop
|
109
|
+
unless fire
|
110
|
+
wait task, text: "Stopping"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def create klass, template, timeout: nil, fire: nil, secs: nil, start: nil, **options
|
115
|
+
options[:start] = true if fire and start
|
116
|
+
task = klass.create template, **options
|
117
|
+
return if fire
|
118
|
+
wait task, text: "Creating"
|
119
|
+
host = task.host.refresh!
|
120
|
+
start host, timeout: timeout, secs: secs
|
121
|
+
end
|
122
|
+
|
123
|
+
def destroy ct, timeout: nil, fire: nil, secs: nil
|
124
|
+
task = ct.destroy
|
125
|
+
unless fire
|
126
|
+
wait task, text: "Destroying"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def help cl, *args
|
131
|
+
STDERR.puts cl.help( *args)
|
132
|
+
exit 1 unless interactive?
|
133
|
+
end
|
134
|
+
|
135
|
+
def opts_wait cl
|
136
|
+
cl.
|
137
|
+
opt( :timeout, "-t", "--timeout=TIMEOUT", "Wait for max TIMEOUT seconds (default: endless)", default: nil).
|
138
|
+
opt( :secs, "-s", "--seconds=SECONDS", "Check every SECONDS for state (default: 0.2)", default: 0.2).
|
139
|
+
opt( :fire, "-f", "--[no-]fire", "Do not wait till running", default: false)
|
140
|
+
end
|
141
|
+
|
142
|
+
def prepare
|
143
|
+
cli_node
|
144
|
+
cli_ct
|
145
|
+
cli_qm
|
146
|
+
cli_task
|
147
|
+
cli_ha
|
148
|
+
cli_base
|
149
|
+
end
|
150
|
+
|
151
|
+
def call *argv
|
152
|
+
cli.call *argv
|
153
|
+
rescue RestClient::ExceptionWithResponse
|
154
|
+
STDERR.puts "#$! - #{$!.response} (#{$!.class})", $!.backtrace.map {|b|" #{b}"}
|
155
|
+
rescue UsageError, DenCli::UsageError
|
156
|
+
STDERR.puts $!
|
157
|
+
exit 1
|
158
|
+
end
|
159
|
+
end
|
data/lib/pve/cli/base.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'pmap'
|
2
|
+
|
3
|
+
class PVE::Cli
|
4
|
+
|
5
|
+
def cli_base
|
6
|
+
cli.cmd :list, "List CT/VM-IDs", aliases: ['ls'], &lambda {|target=nil|
|
7
|
+
connect
|
8
|
+
nodes = Proxmox::Node.all
|
9
|
+
nodes.
|
10
|
+
flat_map {|n| [ n.method(:lxc), n.method(:qemu) ] }.
|
11
|
+
flat_pmap {|m| m.call.map {|c| c.vmid.to_i } }.
|
12
|
+
sort.
|
13
|
+
each {|c| puts c }
|
14
|
+
}
|
15
|
+
|
16
|
+
cli.cmd( :status, "Lists Nodes/VMs/CTs with status", &lambda {|target=nil, sort: 'n', node: nil|
|
17
|
+
connect
|
18
|
+
node &&= /\A#{node}\z/
|
19
|
+
to = TablizedOutput.new %w[Status HA ID Name Host Uptime CPU/% Mem/MiB Mem/% Disk/MiB Disk/%]
|
20
|
+
push =
|
21
|
+
if target
|
22
|
+
target = /\A#{target}\z/
|
23
|
+
lambda {|n| to.virt n if n === target }
|
24
|
+
else
|
25
|
+
lambda {|n| to.virt n }
|
26
|
+
end
|
27
|
+
nodes = Proxmox::Node.all
|
28
|
+
nodes.
|
29
|
+
select {|n| not node or n === node }.
|
30
|
+
flat_map {|n| [ n.method(:lxc), n.method(:qemu) ] }.
|
31
|
+
each {|m| m.call.each &push }
|
32
|
+
to.print order: sort.each_char.map {|c| (2*c.ord[5]-1) * (' sainhucmd'.index( c.downcase)) }
|
33
|
+
}).
|
34
|
+
opt( :sort, '-s', '--sort=COLUMNS', "Sort by COLUMNs eg hn for host and name ([s]tatus, h[a], [i]d, [n]ame (default), [h]ost, [u]ptime, [c]pu, [m]em, [d]isk)").
|
35
|
+
opt( :node, '-n', '--node=NODE', "List only hosted by this NODE")
|
36
|
+
|
37
|
+
def prepare_show_config cnf
|
38
|
+
r = {}
|
39
|
+
cnf.each do |k,v|
|
40
|
+
case k
|
41
|
+
when :network
|
42
|
+
v.each do |net|
|
43
|
+
s =
|
44
|
+
net.
|
45
|
+
reject {|k, v| :card == k }.
|
46
|
+
sort_by {|k, v| :name == k ? :AAAAAAAAA : k }.
|
47
|
+
map {|k, v| case v when true then [k,1] when false then [k,0] else [k,v] end }.
|
48
|
+
map {|k, v| "#{k}=#{v}" }
|
49
|
+
r[net[:card].to_sym] = s.join(",")
|
50
|
+
end
|
51
|
+
when :sshkeys
|
52
|
+
r[k] = CGI.unescape(v).gsub( /^/, " "*14).gsub /\A {14}|\n\z/, ''
|
53
|
+
else
|
54
|
+
case v
|
55
|
+
when true then v = 1
|
56
|
+
when false then v = 0
|
57
|
+
end
|
58
|
+
r[k] = v.to_s.gsub( /$^/, " "*14).gsub /\n\z/, ''
|
59
|
+
end
|
60
|
+
end
|
61
|
+
r
|
62
|
+
end
|
63
|
+
|
64
|
+
def show_config cnf, old = nil
|
65
|
+
cnf = prepare_show_config cnf
|
66
|
+
if old
|
67
|
+
old = prepare_show_config old
|
68
|
+
(cnf.keys+old.keys).uniq.sort.each do |k|
|
69
|
+
v, o = cnf[k], old[k]
|
70
|
+
if v == o
|
71
|
+
puts "#{k}:#{' ' * (12-k.length)} #{v}"
|
72
|
+
else
|
73
|
+
puts "\e[31m#{k}:#{' ' * (12-k.length)} #{o}\e[0m" unless o.nil?
|
74
|
+
puts "\e[32m#{k}:#{' ' * (12-k.length)} #{v}\e[0m" unless v.nil?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
else
|
78
|
+
cnf.sort_by{|k,v|k}.each do |k,v|
|
79
|
+
puts "#{k}:#{' ' * (12-k.length)} #{v}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
cli.sub :config, "CT/VM Configuration", min: 2, aliases: %w[cnf] do |ccli|
|
85
|
+
ccli.cmd 'help', '', aliases: [nil, '-h', '--help'], &lambda {|*args| help ccli, *args }
|
86
|
+
|
87
|
+
ccli.cmd :show, "Show Config of CT/VM", &lambda {|name_or_id|
|
88
|
+
connect
|
89
|
+
th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
|
90
|
+
show_config th.config
|
91
|
+
}
|
92
|
+
|
93
|
+
ccli.cmd :set, "Set Configs for CT/VM", &lambda {|name_or_id, *args|
|
94
|
+
if %w[-h --help].include? name_or_id
|
95
|
+
STDERR.puts "Usage: set -h|--help # Show help"
|
96
|
+
STDERR.puts " set ct|vm --CNF1=VAL1 --CNF2=VAL2 ... # Set config-value. Empty value clears field."
|
97
|
+
exit 1
|
98
|
+
end
|
99
|
+
opts = {}
|
100
|
+
until args.empty?
|
101
|
+
case arg = args.shift
|
102
|
+
when /\A--(\w+)=(.*)\z/
|
103
|
+
opts[$1.to_sym] = $2
|
104
|
+
when /\A--(\w+)\z/
|
105
|
+
opts[$1.to_sym] = args.shift
|
106
|
+
else
|
107
|
+
raise UsageError, "Expection option to set. What do you mean with: #{arg}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
opts.each do |k, v|
|
111
|
+
opts[k] =
|
112
|
+
case v = opts[k]
|
113
|
+
when '' then nil
|
114
|
+
else v
|
115
|
+
end
|
116
|
+
end
|
117
|
+
%i[migrate_downtime].each do |k|
|
118
|
+
next unless opts.has_key? k
|
119
|
+
opts[k] =
|
120
|
+
case v = opts[k]
|
121
|
+
when nil, '', 'nil' then nil
|
122
|
+
else v.to_f
|
123
|
+
end
|
124
|
+
end
|
125
|
+
%i[memory background_delay balloon cores cpulimit cpuunits migrate_speed shares smp sockets vcpus swap tty].each do |k|
|
126
|
+
next unless opts.has_key? k
|
127
|
+
opts[k] =
|
128
|
+
case v = opts[k]
|
129
|
+
when nil, '', 'nil' then nil
|
130
|
+
else v.to_i
|
131
|
+
end
|
132
|
+
end
|
133
|
+
%i[unprivileged debug onboot protection template].each do |k|
|
134
|
+
next unless opts.has_key? k
|
135
|
+
opts[k] =
|
136
|
+
case v = opts[k]
|
137
|
+
when *%w[1 T TRUE t true True Y YES y yes Yes] then true
|
138
|
+
when *%w[0 F FALSE f false False N NO n no No] then false
|
139
|
+
when '', 'nil', nil then nil
|
140
|
+
else raise UsageError, "Boolean expected, given: #{v.inspect}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
connect
|
144
|
+
th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
|
145
|
+
old = th.config
|
146
|
+
opts[:digest] ||= old[:digest]
|
147
|
+
th.cnfset opts
|
148
|
+
show_config th.config, old
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
cli.cmd :enter, "Enter Console of CT/Node", &lambda {|name_or_id|
|
153
|
+
connect
|
154
|
+
th = Proxmox::LXC.find( name_or_id) || Proxmox::Node.find_by_name( name_or_id)
|
155
|
+
raise UsageError, "Container or Node not found: #{name_or_id}" unless th
|
156
|
+
STDERR.puts "! #{$?.exitstatus}" unless th.enter
|
157
|
+
}
|
158
|
+
|
159
|
+
cli.cmd :run, "Starts CT/VM", aliases: %w[start star], &lambda {|name_or_id|
|
160
|
+
connect
|
161
|
+
th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
|
162
|
+
raise UsageError, "Container or Node not found: #{name_or_id}" unless th
|
163
|
+
start th
|
164
|
+
}
|
165
|
+
|
166
|
+
#cli.cmd :reboot, "Reboot CT/VM (not implemented, yet)", min: 6, &lambda {|name_or_id|
|
167
|
+
# connect
|
168
|
+
# th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
|
169
|
+
# raise UsageError, "Container or Node not found: #{name_or_id}" unless th
|
170
|
+
# reboot th
|
171
|
+
#}
|
172
|
+
|
173
|
+
cli.cmd :stop, "Stops CT/VM", min: 4, &lambda {|name_or_id|
|
174
|
+
connect
|
175
|
+
th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
|
176
|
+
raise UsageError, "Container or Node not found: #{name_or_id}" unless th
|
177
|
+
stop th
|
178
|
+
}
|
179
|
+
|
180
|
+
cli.cmd 'help', '', aliases: ['-h', '--help'], &lambda {|*args| help cli, *args }
|
181
|
+
|
182
|
+
cli.cmd 'cli', 'Opens interactive console', min: 3, aliases: [nil], &lambda {
|
183
|
+
@interactive = true
|
184
|
+
cli.interactive( File.basename($0,'.rb')).run
|
185
|
+
}
|
186
|
+
end
|
187
|
+
end
|