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.
@@ -0,0 +1,597 @@
1
+ require 'rest_client'
2
+ require 'cgi'
3
+ require 'json'
4
+ require 'ipaddress'
5
+ require 'shellwords'
6
+ require 'active_support/all'
7
+
8
+ module Proxmox
9
+ class Exception < ::Exception
10
+ end
11
+ class NotFound < Exception
12
+ end
13
+ class AlreadyExists < Exception
14
+ end
15
+
16
+ def self.connect username, password, realm: nil, verify_tls: nil, uri: nil
17
+ uri ||= 'https://localhost:8006/api2/json'
18
+ cred =
19
+ {
20
+ username: username,
21
+ password: password,
22
+ realm: realm || 'pve'
23
+ }
24
+ @@connection =
25
+ RestClient::Resource.new( uri, verify_ssl: ! ! verify_tls).tap do |rs|
26
+ resp = rs['/access/ticket'].post cred
27
+ case resp.code
28
+ when 200
29
+ data = JSON.parse resp.body, symbolize_names: true
30
+ @@api_ticket =
31
+ {
32
+ CSRFPreventionToken: data[:data][:CSRFPreventionToken],
33
+ cookie: "PVEAuthCookie=#{CGI.escape data[:data][:ticket]}"
34
+ }
35
+ else
36
+ raise Exception, "Authentication failed"
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.connection
42
+ @@connection
43
+ end
44
+
45
+ def self.api_ticket
46
+ @@api_ticket
47
+ end
48
+
49
+ def self.find_by_name name
50
+ Proxmox::LXC.find_by_name( name) || Proxmox::Qemu.find_by_name( name) || Proxmox::Node.find_by_name( name)
51
+ end
52
+
53
+ def self.find_by_vmid vmid
54
+ Proxmox::LXC.find_by_vmid( vmid) || Proxmox::Qemu.find_by_vmid( vmid) || Proxmox::Node.find_by_vmid( vmid)
55
+ end
56
+
57
+ def self.find name_or_id
58
+ Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find( name_or_id) || Proxmox::Node.find( name_or_id)
59
+ end
60
+
61
+ module RestConnection
62
+ def __response__ resp
63
+ case resp.code
64
+ when 200
65
+ JSON.parse( resp.body, symbolize_names: true)[:data]
66
+ when 500
67
+ nil
68
+ else
69
+ raise "Request failed of #{req.url} [#{resp.code}]: #{resp.body.inspect}"
70
+ end
71
+ end
72
+
73
+ def __headers__ **hdrs
74
+ Proxmox.api_ticket.merge( 'Accept' => 'application/json').merge( hdrs)
75
+ end
76
+
77
+ def __data__ **data
78
+ case data
79
+ when String
80
+ data
81
+ else
82
+ data.to_json
83
+ end
84
+ end
85
+
86
+ private :__response__, :__headers__, :__data__
87
+
88
+ def bench path
89
+ #t = Time.now
90
+ r = yield
91
+ #p path => Time.now-t
92
+ r
93
+ end
94
+
95
+ def rest_get path, **data, &exe
96
+ data = data.delete_if {|k,v|v.nil?}
97
+ path += "#{path.include?( ??) ? ?& : ??}#{data.map{|k,v|"#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"}.join '&'}" unless data.empty?
98
+ __response__ Proxmox.connection[path].get( __headers__( :'Content-Type' => 'application/json'))
99
+ end
100
+
101
+ def rest_put path, **data, &exe
102
+ __response__ Proxmox.connection[path].put( __data__( data), __headers__( :'Content-Type' => 'application/json'))
103
+ end
104
+
105
+ def rest_del path, &exe
106
+ __response__ Proxmox.connection[path].delete( __headers__( :'Content-Type' => 'application/json'))
107
+ end
108
+
109
+ def rest_post path, **data, &exe
110
+ __response__ Proxmox.connection[path].post( __data__( data), __headers__( :'Content-Type' => 'application/json'))
111
+ end
112
+ end
113
+
114
+ class Base
115
+ include RestConnection
116
+ extend RestConnection
117
+
118
+ attr_reader :sid
119
+
120
+ class <<self
121
+ def __new__ data
122
+ n = allocate
123
+ n.send :__update__, data
124
+ end
125
+ private :__new__
126
+ end
127
+
128
+ def respond_to? method
129
+ super or instance_variable_defined?( "@#{method}")
130
+ end
131
+
132
+ def method_missing method, *args, &exe
133
+ if instance_variable_defined? "@#{method}"
134
+ instance_variable_get "@#{method}"
135
+ else
136
+ super
137
+ end
138
+ end
139
+
140
+ def __update__ data
141
+ instance_variables.each do |k|
142
+ remove_instance_variable k
143
+ end
144
+ data.each do |k,v|
145
+ instance_variable_set "@#{k}", v
146
+ end
147
+ initialize
148
+ self
149
+ end
150
+
151
+ def refresh!
152
+ __update__ rest_get( @rest_prefix)
153
+ end
154
+ end
155
+
156
+ class Node < Base
157
+ class <<self
158
+ def find_by_name name
159
+ all.each do |node|
160
+ return node if node.node == name
161
+ end
162
+ nil
163
+ end
164
+
165
+ def find_by_name! name
166
+ find_by_name( name) or raise( Proxmox::NotFound, "Node not found: #{name}")
167
+ end
168
+
169
+ def all
170
+ rest_get( '/nodes').map {|d| __new__ d.merge( t: 'nd') }
171
+ end
172
+ end
173
+
174
+ attr_reader :name
175
+
176
+ def === t
177
+ @name =~ t or @vmid.to_s =~ t or @sid =~ t
178
+ end
179
+
180
+ def initialize
181
+ @rest_prefix = "/nodes/#{@node}"
182
+ @sid = "nd:#{@node}"
183
+ @name = @node
184
+ end
185
+
186
+ def offline?() @status.nil? or 'offline' == @status end
187
+ def online?() 'online' == @status end
188
+
189
+ def lxc
190
+ return [] if offline?
191
+ rest_get( "#{@rest_prefix}/lxc").map {|d| LXC.send :__new__, d.merge( node: self, t: 'ct') }
192
+ end
193
+
194
+ def qemu
195
+ return [] if offline?
196
+ rest_get( "#{@rest_prefix}/qemu").map {|d| Qemu.send :__new__, d.merge( node: self, t: 'qm') }
197
+ end
198
+
199
+ def tasks
200
+ return [] if offline?
201
+ rest_get( "/#{@rest_prefix}/tasks").map {|d| Task.send :__new__, d.merge( node: self, t: 'task') }
202
+ end
203
+
204
+ def enter *command
205
+ Kernel.system 'ssh', '-t', node, command.map( &:to_s).shelljoin
206
+ end
207
+
208
+ def exec *command
209
+ Kernel.system 'ssh', node, command.map( &:to_s).shelljoin
210
+ end
211
+ end
212
+
213
+ class Task < Base
214
+ def initialize
215
+ @rest_prefix = "/nodes/#{@node.node}/tasks/#{upid}"
216
+ @sid = upid
217
+ end
218
+
219
+ def inspect
220
+ "#<#{self.class.name} #{upid}>"
221
+ end
222
+
223
+ #def finished?
224
+ # rest_get( "/nodes/#{node}/tasks/")
225
+ #end
226
+
227
+ def status
228
+ rest_get( "#{@rest_prefix}/status")
229
+ end
230
+
231
+ def log start: nil, limit: nil
232
+ rest_get( "#{@rest_prefix}/log", start: start, limit: limit)
233
+ end
234
+ end
235
+
236
+ class Hosted < Base
237
+ def refresh!
238
+ node, t = @node, @t
239
+ __update__ rest_get( "#{@rest_prefix}/status/current").merge( node: node, t: t)
240
+ end
241
+
242
+ def === t
243
+ @name =~ t or @vmid.to_s =~ t or @sid =~ t
244
+ end
245
+
246
+ def migrate node
247
+ node =
248
+ case node
249
+ when Node then node
250
+ else Node.find!( node.to_s)
251
+ end
252
+ Task.send :__new__, node: @node, host: self, upid: rest_post( "#{@rest_prefix}/migrate", target: node.node)
253
+ end
254
+
255
+ def start
256
+ Task.send :__new__, node: @node, host: self, upid: rest_post( "#{@rest_prefix}/status/start")
257
+ end
258
+
259
+ def stop
260
+ Task.send :__new__, node: @node, host: self, upid: rest_post( "#{@rest_prefix}/status/stop")
261
+ end
262
+
263
+ def destroy
264
+ Task.send :__new__, node: @node, host: self, upid: rest_del( "#{@rest_prefix}")
265
+ end
266
+
267
+ def current_status
268
+ rest_get "#{@rest_prefix}/status/current"
269
+ end
270
+
271
+ def running?
272
+ current_status[:status] == 'running'
273
+ end
274
+
275
+ def stopped?
276
+ current_status[:status] == 'stopped'
277
+ end
278
+
279
+ def wait forstatus, timeout: nil, secs: nil, lock: nil, &exe
280
+ forstatus = forstatus.to_s
281
+ secs ||= 0.2
282
+ b = Time.now + timeout.to_i + secs
283
+ loop do
284
+ d = current_status
285
+ return true if d[:status] == forstatus and (not lock or lock == d[:lock])
286
+ exe.call d[:status], d[:lock] if exe
287
+ return false if timeout and b <= Time.now
288
+ sleep secs
289
+ end
290
+ nil
291
+ end
292
+
293
+ def config
294
+ cnf = rest_get "#{@rest_prefix}/config"
295
+ cnf[:network] =
296
+ cnf.
297
+ keys.
298
+ map( &:to_s).
299
+ grep( /\Anet\d+\z/).
300
+ map do |k|
301
+ nc = {card: k}
302
+ cnf.delete( k.to_sym).
303
+ split( ',').
304
+ each do |f|
305
+ k, v = f.split( '=', 2)
306
+ nc[k.to_sym] = v
307
+ end
308
+ nc[:ip] &&= IPAddress::IPv4.new nc[:ip]
309
+ nc[:gw] &&= IPAddress::IPv4.new nc[:gw]
310
+ nc[:mtu] &&= nc[:mtu].to_i
311
+ nc[:tag] &&= nc[:tag].to_i
312
+ nc[:firewall] &&= 1 == nc[:firewall].to_i
313
+ nc
314
+ end
315
+ cnf[:unprivileged] &&= 1 == cnf[:unprivileged]
316
+ cnf[:memory] &&= cnf[:memory].to_i
317
+ cnf[:cores] &&= cnf[:cores].to_i
318
+ cnf
319
+ end
320
+
321
+ def cnfset **cnf
322
+ r = {delete: []}
323
+ cnf.each do |k,v|
324
+ case v
325
+ when true then r[k] = 1
326
+ when false then r[k] = 0
327
+ when nil then r[:delete].push k
328
+ else r[k] = v
329
+ end
330
+ end
331
+ r.delete :delete if r[:delete].empty?
332
+ rest_put "#{@rest_prefix}/config", r
333
+ end
334
+ end
335
+
336
+ class Qemu < Hosted
337
+ class <<self
338
+ def all
339
+ Node.all.flat_map {|n| n.qemu }
340
+ end
341
+
342
+ def find_by_vmid vmid
343
+ vmid = vmid.to_s
344
+ Node.all.each do |n|
345
+ n.qemu.each do |l|
346
+ return l if l.vmid == vmid
347
+ end
348
+ end
349
+ nil
350
+ end
351
+
352
+ def find_by_name name
353
+ Node.all.each do |n|
354
+ n.qemu.each do |l|
355
+ return l if l.name == name
356
+ end
357
+ end
358
+ nil
359
+ end
360
+
361
+ def find name_or_id = nil, name: nil, vmid: nil
362
+ if (name and vmid) or (name and name_or_id) or (name_or_id and vmid)
363
+ raise Proxmox::NotFound, "name xor vmid needed to find CT, not both."
364
+ elsif name
365
+ find_by_name name
366
+ elsif vmid
367
+ find_by_vmid vmid.to_i
368
+ elsif name_or_id =~ /\D/
369
+ find_by_name name_or_id
370
+ elsif name_or_id.is_a?( Numeric) or name_or_id =~ /\d/
371
+ find_by_vmid name_or_id.to_i
372
+ else
373
+ raise Proxmox::NotFound, "name xor vmid needed to find CT."
374
+ end
375
+ end
376
+
377
+ def find_by_vmid! name
378
+ find_by_vmid( name) or raise( Proxmox::NotFound, "Virtual Machine not found: #{name}")
379
+ end
380
+
381
+ def find_by_name! name
382
+ find_by_name( name) or raise( Proxmox::NotFound, "Virtual Machine not found: #{name}")
383
+ end
384
+
385
+ def find! name
386
+ find( name) or raise( Proxmox::NotFound, "Virtual Machine not found: #{name}")
387
+ end
388
+ end
389
+
390
+ def initialize
391
+ @rest_prefix = "/nodes/%s/qemu/%d" % [@node.node, @vmid]
392
+ @sid = "qm:#{@vmid}"
393
+ end
394
+
395
+ def ha
396
+ HA.find self
397
+ end
398
+
399
+ def exec *args
400
+ node.exec 'qm', 'guest', 'exec', vmid, '--', *args
401
+ end
402
+ end
403
+
404
+ class LXC < Hosted
405
+ class <<self
406
+ def all
407
+ Node.all.flat_map {|n| n.lxc }
408
+ end
409
+
410
+ def find_by_vmid vmid
411
+ vmid = vmid.to_s
412
+ Node.all.each do |n|
413
+ n.lxc.each do |l|
414
+ return l if l.vmid == vmid
415
+ end
416
+ end
417
+ nil
418
+ end
419
+
420
+ def find_by_name name
421
+ Node.all.each do |n|
422
+ n.lxc.each do |l|
423
+ return l if l.name == name
424
+ end
425
+ end
426
+ nil
427
+ end
428
+
429
+ def find name_or_id = nil, name: nil, vmid: nil
430
+ if (name and vmid) or (name and name_or_id) or (name_or_id and vmid)
431
+ raise ArgumentError, "name xor vmid needed to find CT, not both."
432
+ elsif name
433
+ find_by_name name
434
+ elsif vmid
435
+ find_by_vmid vmid.to_i
436
+ elsif name_or_id =~ /\D/
437
+ find_by_name name_or_id
438
+ elsif name_or_id.is_a?( Numeric) or name_or_id =~ /\d/
439
+ find_by_vmid name_or_id.to_i
440
+ else
441
+ raise ArgumentError, "name xor vmid needed to find CT."
442
+ end
443
+ end
444
+
445
+ def find_by_vmid! name
446
+ find_by_vmid( name) or raise( Proxmox::NotFound, "Container not found: #{name}")
447
+ end
448
+
449
+ def find_by_name! name
450
+ find_by_name( name) or raise( Proxmox::NotFound, "Container not found: #{name}")
451
+ end
452
+
453
+ def find! name
454
+ find( name) or raise( Proxmox::NotFound, "Container not found: #{name}")
455
+ end
456
+
457
+ def create template, **options
458
+ tmplt = PVE::CTTemplate.const_get( template&.classify || 'Default').new **options
459
+ name = tmplt.name
460
+ virts = Proxmox::LXC.all + Proxmox::Qemu.all
461
+ vmid = tmplt.vmid
462
+ if virt = virts.find {|v| v.vmid == vmid }
463
+ raise Proxmox::AlreadyExists, "VT/VM with vmid [#{vmid}] already exists: #{virt.name}"
464
+ end
465
+ already_exists = virts.select {|v| v.name == name }
466
+ unless already_exists.empty?
467
+ raise Proxmox::AlreadyExists, "CT/VM named [#{name}] already exists: vmid: #{already_exists.map( &:vmid).inspect}"
468
+ end
469
+ node = Proxmox::Node.find_by_name! tmplt.node
470
+
471
+ options = {
472
+ ostemplate: tmplt.ostemplate,
473
+ vmid: vmid.to_s,
474
+ arch: tmplt.arch,
475
+ cmode: tmplt.cmode,
476
+ cores: tmplt.cores.to_i,
477
+ description: tmplt.description,
478
+ hostname: tmplt.hostname,
479
+ memory: tmplt.memory,
480
+ net0: tmplt.net0&.map {|k,v| "#{k}=#{v}" }&.join(','),
481
+ net1: tmplt.net1&.map {|k,v| "#{k}=#{v}" }&.join(','),
482
+ net2: tmplt.net2&.map {|k,v| "#{k}=#{v}" }&.join(','),
483
+ net3: tmplt.net3&.map {|k,v| "#{k}=#{v}" }&.join(','),
484
+ ostype: tmplt.ostype,
485
+ :'ssh-public-keys' => tmplt.ssh_public_keys,
486
+ storage: tmplt.storage,
487
+ #rootfs: {
488
+ # volume: "root:vm-#{vmid}-disk-0", size: '4G'
489
+ #}.map {|k,v| "#{k}=#{v}" }.join( ','),
490
+ swap: tmplt.swap,
491
+ unprivileged: tmplt.unprivileged,
492
+ }.delete_if {|k,v| v.nil? }
493
+ @temp = LXC.send :__new__, node: node, vmid: options[:vmid], name: name, hostname: options[:hostname]
494
+ upid = rest_post( "/nodes/%s/lxc" % node.node, **options)
495
+ Task.send :__new__, node: node, host: @temp, upid: upid
496
+ end
497
+ end
498
+
499
+ def initialize
500
+ @rest_prefix = "/nodes/%s/lxc/%d" % [@node.node, @vmid]
501
+ @sid = "ct:#{@vmid}"
502
+ end
503
+
504
+ def ha
505
+ HA.find self
506
+ end
507
+
508
+ def exec *args
509
+ node.exec 'pct', 'exec', vmid, '--', *args
510
+ end
511
+
512
+ def enter
513
+ node.enter 'pct', 'enter', vmid
514
+ end
515
+ end
516
+
517
+ class HA < Base
518
+ def initialize
519
+ @rest_prefix = "/cluster/ha/resources/#{virt.sid}"
520
+ end
521
+
522
+ class <<self
523
+ def find host
524
+ ha = HA.send :__new__, digest: nil, state: nil, virt: host
525
+ ha.refresh!
526
+ end
527
+
528
+ def create host, **options
529
+ find( host).create **options
530
+ end
531
+ end
532
+
533
+ def refresh!
534
+ virt = @virt
535
+ __update__( {state: nil}.merge( rest_get( "/cluster/ha/resources/#{virt.sid}")).merge( virt: virt))
536
+ self
537
+ rescue RestClient::InternalServerError
538
+ __update__ digest: nil, state: nil, virt: virt
539
+ end
540
+
541
+ def create group: nil, comment: nil, max_relocate: nil, max_restart: nil, state: nil
542
+ options = {
543
+ sid: virt.sid,
544
+ group: group,
545
+ comment: comment,
546
+ max_relocate: max_relocate,
547
+ max_restart: max_restart,
548
+ state: state
549
+ }
550
+ options.delete_if {|k,v| v.nil? }
551
+ rest_post "/cluster/ha/resources", **options
552
+ refresh!
553
+ end
554
+
555
+ def delete
556
+ rest_del "#{@rest_prefix}"
557
+ end
558
+
559
+ def state= state
560
+ rest_put "#{@rest_prefix}", state: state.to_s
561
+ refresh!
562
+ end
563
+
564
+ def started!
565
+ self.state = :started
566
+ self
567
+ end
568
+
569
+ def stopped!
570
+ self.state = :stopped
571
+ self
572
+ end
573
+
574
+ def disabled!
575
+ self.state = :disabled
576
+ self
577
+ end
578
+
579
+ def started?
580
+ 'started' == self.state
581
+ end
582
+
583
+ def stopped?
584
+ 'stopped' == self.state
585
+ end
586
+
587
+ def error?
588
+ 'error' == self.state
589
+ end
590
+
591
+ def active?
592
+ ! ! digest
593
+ end
594
+ end
595
+ end
596
+
597
+ require_relative 'templates'