pve 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|