pve 0.1.2

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