pve 0.1.2

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