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 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
@@ -0,0 +1,2 @@
1
+ *.sw[pomnqrst]
2
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pathname'
3
+ $:.unshift Pathname.new(__FILE__).dirname.dirname.join('lib').to_s
4
+ require 'pve/cli'
5
+ PVE::Cli.new.call *ARGV
data/lib/pve.rb ADDED
@@ -0,0 +1,4 @@
1
+ module PVE
2
+ end
3
+
4
+ require_relative 'pve/proxmox'
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
@@ -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