line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
#!/bin/false |
2
|
|
|
|
|
|
|
# vim: softtabstop=2 tabstop=2 shiftwidth=2 ft=perl expandtab smarttab |
3
|
|
|
|
|
|
|
# PODNAME: Net::Proxmox::VE |
4
|
|
|
|
|
|
|
# ABSTRACT: Pure perl API for Proxmox virtualisation |
5
|
|
|
|
|
|
|
|
6
|
1
|
|
|
1
|
|
71047
|
use strict; |
|
1
|
|
|
|
|
9
|
|
|
1
|
|
|
|
|
30
|
|
7
|
1
|
|
|
1
|
|
5
|
use warnings; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
46
|
|
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
package Net::Proxmox::VE; |
10
|
|
|
|
|
|
|
$Net::Proxmox::VE::VERSION = '0.38'; |
11
|
|
|
|
|
|
|
|
12
|
1
|
|
|
1
|
|
5
|
use Carp qw( croak ); |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
55
|
|
13
|
1
|
|
|
1
|
|
572
|
use HTTP::Headers; |
|
1
|
|
|
|
|
7074
|
|
|
1
|
|
|
|
|
38
|
|
14
|
1
|
|
|
1
|
|
522
|
use HTTP::Request::Common qw(GET POST DELETE); |
|
1
|
|
|
|
|
19721
|
|
|
1
|
|
|
|
|
67
|
|
15
|
1
|
|
|
1
|
|
492
|
use JSON::MaybeXS qw(decode_json); |
|
1
|
|
|
|
|
5743
|
|
|
1
|
|
|
|
|
56
|
|
16
|
1
|
|
|
1
|
|
700
|
use LWP::UserAgent; |
|
1
|
|
|
|
|
31037
|
|
|
1
|
|
|
|
|
34
|
|
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
# done |
19
|
1
|
|
|
1
|
|
425
|
use Net::Proxmox::VE::Pools; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
62
|
|
20
|
1
|
|
|
1
|
|
435
|
use Net::Proxmox::VE::Storage; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
53
|
|
21
|
1
|
|
|
1
|
|
465
|
use Net::Proxmox::VE::Access; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
123
|
|
22
|
1
|
|
|
1
|
|
479
|
use Net::Proxmox::VE::Cluster; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
111
|
|
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
# wip |
25
|
1
|
|
|
1
|
|
533
|
use Net::Proxmox::VE::Nodes; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
1690
|
|
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
sub action { |
29
|
|
|
|
|
|
|
|
30
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
31
|
0
|
|
|
|
|
|
my %params = @_; |
32
|
|
|
|
|
|
|
|
33
|
0
|
0
|
|
|
|
|
unless (%params) { |
34
|
0
|
|
|
|
|
|
croak 'new requires a hash for params'; |
35
|
|
|
|
|
|
|
} |
36
|
0
|
0
|
|
|
|
|
croak 'path param is required' unless $params{path}; |
37
|
|
|
|
|
|
|
|
38
|
0
|
|
0
|
|
|
|
$params{method} ||= 'GET'; |
39
|
0
|
|
0
|
|
|
|
$params{post_data} ||= {}; |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
# Check its a valid method |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
croak "invalid http method specified: $params{method}" |
44
|
0
|
0
|
|
|
|
|
unless $params{method} =~ m/^(GET|PUT|POST|DELETE)$/; |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
# Strip prefixed / to path if present |
47
|
0
|
|
|
|
|
|
$params{path} =~ s{^/}{}; |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
# Collapse duplicate slashes |
50
|
0
|
|
|
|
|
|
$params{path} =~ s{//+}{/}; |
51
|
|
|
|
|
|
|
|
52
|
0
|
0
|
0
|
|
|
|
unless ( $params{path} eq 'access/domains' |
53
|
|
|
|
|
|
|
or $self->check_login_ticket ) |
54
|
|
|
|
|
|
|
{ |
55
|
|
|
|
|
|
|
print "DEBUG: invalid login ticket\n" |
56
|
0
|
0
|
|
|
|
|
if $self->{params}->{debug}; |
57
|
0
|
0
|
|
|
|
|
return unless $self->login(); |
58
|
|
|
|
|
|
|
} |
59
|
|
|
|
|
|
|
|
60
|
0
|
|
|
|
|
|
my $url = $self->url_prefix . '/api2/json/' . $params{path}; |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
# Grab the useragent |
63
|
0
|
|
|
|
|
|
my $ua = $self->{ua}; |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
# Set up the request object |
66
|
0
|
|
|
|
|
|
my $request = HTTP::Request->new(); |
67
|
0
|
|
|
|
|
|
$request->uri($url); |
68
|
|
|
|
|
|
|
$request->header( 'Cookie' => 'PVEAuthCookie=' . $self->{ticket}->{ticket} ) |
69
|
0
|
0
|
|
|
|
|
if defined $self->{ticket}; |
70
|
|
|
|
|
|
|
|
71
|
0
|
|
|
|
|
|
my $response; |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
# all methods other than get require the prevention token |
74
|
|
|
|
|
|
|
# (ie anything that makes modification) |
75
|
0
|
0
|
|
|
|
|
unless ( $params{method} eq 'GET' ) { |
76
|
|
|
|
|
|
|
$request->header( |
77
|
0
|
|
|
|
|
|
'CSRFPreventionToken' => $self->{ticket}->{CSRFPreventionToken} ); |
78
|
|
|
|
|
|
|
} |
79
|
|
|
|
|
|
|
|
80
|
0
|
0
|
|
|
|
|
if ( $params{method} =~ m/^(PUT|POST)$/ ) { |
|
|
0
|
|
|
|
|
|
81
|
0
|
|
|
|
|
|
$request->method( $params{method} ); |
82
|
0
|
|
|
|
|
|
my $content = join '&', map { $_ . '=' . $params{post_data}->{$_} } |
83
|
0
|
|
|
|
|
|
sort keys %{ $params{post_data} }; |
|
0
|
|
|
|
|
|
|
84
|
0
|
|
|
|
|
|
$request->content($content); |
85
|
0
|
|
|
|
|
|
$response = $ua->request($request); |
86
|
|
|
|
|
|
|
} |
87
|
|
|
|
|
|
|
elsif ( $params{method} =~ m/^(GET|DELETE)$/ ) { |
88
|
0
|
|
|
|
|
|
$request->method( $params{method} ); |
89
|
0
|
0
|
|
|
|
|
if ( %{$params{post_data}} ) { |
|
0
|
|
|
|
|
|
|
90
|
0
|
|
|
|
|
|
my $qstring = join '&', map { $_ . '=' . $params{post_data}->{$_} } |
91
|
0
|
|
|
|
|
|
sort keys %{ $params{post_data} }; |
|
0
|
|
|
|
|
|
|
92
|
0
|
|
|
|
|
|
$request->uri( "$url?$qstring" ); |
93
|
|
|
|
|
|
|
} |
94
|
0
|
|
|
|
|
|
$response = $ua->request($request); |
95
|
|
|
|
|
|
|
} |
96
|
|
|
|
|
|
|
else { |
97
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
# this shouldnt happen |
99
|
0
|
|
|
|
|
|
croak 'this shouldnt happen'; |
100
|
|
|
|
|
|
|
} |
101
|
|
|
|
|
|
|
|
102
|
0
|
0
|
|
|
|
|
if ( $response->is_success ) { |
103
|
|
|
|
|
|
|
print "DEBUG: successful request: " . $request->as_string . "\n" |
104
|
0
|
0
|
|
|
|
|
if $self->{params}->{debug}; |
105
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
# my $content = $response->decoded_content; |
107
|
0
|
|
|
|
|
|
my $data = decode_json( $response->decoded_content ); |
108
|
|
|
|
|
|
|
|
109
|
0
|
0
|
0
|
|
|
|
if ( ref $data eq 'HASH' |
110
|
|
|
|
|
|
|
&& exists $data->{data} ) |
111
|
|
|
|
|
|
|
{ |
112
|
0
|
0
|
|
|
|
|
if ( ref $data->{data} eq 'ARRAY' ) { |
113
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
return wantarray |
115
|
0
|
|
|
|
|
|
? @{ $data->{data} } |
116
|
0
|
0
|
|
|
|
|
: $data->{data}; |
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
} |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
return $data->{data} |
121
|
|
|
|
|
|
|
|
122
|
0
|
|
|
|
|
|
} |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
# just return true |
125
|
0
|
|
|
|
|
|
return 1 |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
} |
128
|
|
|
|
|
|
|
else { |
129
|
0
|
|
|
|
|
|
croak "WARNING: request failed: " . $request->as_string . "\n" . |
130
|
|
|
|
|
|
|
"WARNING: response status: " . $response->status_line . "\n"; |
131
|
|
|
|
|
|
|
} |
132
|
|
|
|
|
|
|
return |
133
|
|
|
|
|
|
|
|
134
|
0
|
|
|
|
|
|
} |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
sub api_version { |
138
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
139
|
0
|
|
|
|
|
|
return $self->action( path => '/version', method => 'GET' ); |
140
|
|
|
|
|
|
|
} |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
sub api_version_check { |
144
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
145
|
|
|
|
|
|
|
|
146
|
0
|
|
|
|
|
|
my $data = $self->api_version; |
147
|
|
|
|
|
|
|
|
148
|
0
|
0
|
0
|
|
|
|
if ( ref $data eq 'HASH' && $data->{version} ) { |
149
|
0
|
|
|
|
|
|
my ($version) = $data->{version} =~ m/^(\d+)/; |
150
|
0
|
0
|
|
|
|
|
return 1 if $version > 2.0; |
151
|
|
|
|
|
|
|
} |
152
|
|
|
|
|
|
|
|
153
|
0
|
|
|
|
|
|
return; |
154
|
|
|
|
|
|
|
} |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
sub debug { |
158
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
159
|
0
|
|
|
|
|
|
my $d = shift; |
160
|
|
|
|
|
|
|
|
161
|
0
|
0
|
|
|
|
|
if ($d) { |
|
|
0
|
|
|
|
|
|
162
|
0
|
|
|
|
|
|
$self->{params}->{debug} = 1; |
163
|
|
|
|
|
|
|
} |
164
|
|
|
|
|
|
|
elsif ( defined $d ) { |
165
|
0
|
|
|
|
|
|
$self->{params}->{debug} = 0; |
166
|
|
|
|
|
|
|
} |
167
|
|
|
|
|
|
|
|
168
|
0
|
0
|
|
|
|
|
return 1 if $self->{params}->{debug}; |
169
|
|
|
|
|
|
|
return |
170
|
|
|
|
|
|
|
|
171
|
0
|
|
|
|
|
|
} |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
sub delete { |
175
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
176
|
0
|
0
|
|
|
|
|
my @path = @_ or return; # using || breaks this |
177
|
|
|
|
|
|
|
|
178
|
0
|
0
|
|
|
|
|
if ( $self->nodes ) { |
179
|
0
|
|
|
|
|
|
return $self->action( path => join( '/', @path ), method => 'DELETE' ); |
180
|
|
|
|
|
|
|
} |
181
|
|
|
|
|
|
|
return |
182
|
0
|
|
|
|
|
|
} |
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
sub get { |
186
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
187
|
0
|
|
|
|
|
|
my $post_data; |
188
|
0
|
0
|
|
|
|
|
$post_data = pop |
189
|
|
|
|
|
|
|
if ref $_[-1]; |
190
|
0
|
0
|
|
|
|
|
my @path = @_ or return; # using || breaks this |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
# Calling nodes method here would call get method itself and so on |
193
|
|
|
|
|
|
|
# Commented out to avoid an infinite loop |
194
|
|
|
|
|
|
|
#if ( $self->nodes ) { |
195
|
0
|
|
|
|
|
|
return $self->action( path => join( '/', @path ), method => 'GET', post_data => $post_data ); |
196
|
|
|
|
|
|
|
#} |
197
|
0
|
|
|
|
|
|
return; |
198
|
|
|
|
|
|
|
} |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
sub new { |
202
|
|
|
|
|
|
|
|
203
|
0
|
|
|
0
|
1
|
|
my $c = shift; |
204
|
0
|
|
|
|
|
|
my @p = @_; |
205
|
0
|
|
0
|
|
|
|
my $class = ref($c) || $c; |
206
|
|
|
|
|
|
|
|
207
|
0
|
|
|
|
|
|
my %params; |
208
|
|
|
|
|
|
|
|
209
|
0
|
0
|
|
|
|
|
if ( scalar @p == 1 ) { |
|
|
0
|
|
|
|
|
|
210
|
|
|
|
|
|
|
|
211
|
0
|
0
|
|
|
|
|
croak 'new() requires a hash for params' |
212
|
|
|
|
|
|
|
unless ref $p[0] eq 'HASH'; |
213
|
|
|
|
|
|
|
|
214
|
0
|
|
|
|
|
|
%params = %{ $p[0] }; |
|
0
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
} |
217
|
|
|
|
|
|
|
elsif ( scalar @p % 2 != 0 ) { # 'unless' is better than != but anyway |
218
|
0
|
|
|
|
|
|
croak 'new() called with an odd number of parameters' |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
} |
221
|
|
|
|
|
|
|
else { |
222
|
0
|
0
|
|
|
|
|
%params = @p |
223
|
|
|
|
|
|
|
or croak 'new() requires a hash for params'; |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
|
226
|
0
|
|
0
|
|
|
|
my $host = delete $params{host} || croak 'host param is required'; |
227
|
0
|
|
0
|
|
|
|
my $password = delete $params{password} || croak 'password param is required'; |
228
|
0
|
|
0
|
|
|
|
my $port = delete $params{port} || 8006; |
229
|
0
|
|
0
|
|
|
|
my $username = delete $params{username} || 'root'; |
230
|
0
|
|
0
|
|
|
|
my $realm = delete $params{realm} || 'pam'; |
231
|
0
|
|
|
|
|
|
my $debug = delete $params{debug}; |
232
|
0
|
|
0
|
|
|
|
my $timeout = delete $params{timeout} || 10; |
233
|
0
|
|
|
|
|
|
my $ssl_opts = delete $params{ssl_opts}; |
234
|
0
|
0
|
|
|
|
|
croak 'unknown parameters to new: ' . join(', ', keys %params) if keys %params; |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
my $self->{params} = { |
237
|
0
|
|
|
|
|
|
host => $host, |
238
|
|
|
|
|
|
|
password => $password, |
239
|
|
|
|
|
|
|
port => $port, |
240
|
|
|
|
|
|
|
username => $username, |
241
|
|
|
|
|
|
|
realm => $realm, |
242
|
|
|
|
|
|
|
debug => $debug, |
243
|
|
|
|
|
|
|
timeout => $timeout, |
244
|
|
|
|
|
|
|
ssl_opts => $ssl_opts, |
245
|
|
|
|
|
|
|
}; |
246
|
|
|
|
|
|
|
|
247
|
0
|
|
|
|
|
|
$self->{'ticket'} = undef; |
248
|
0
|
|
|
|
|
|
$self->{'ticket_timestamp'} = undef; |
249
|
0
|
|
|
|
|
|
$self->{'ticket_life'} = 7200; # 2 Hours |
250
|
|
|
|
|
|
|
|
251
|
0
|
|
|
|
|
|
my %lwpUserAgentOptions; |
252
|
0
|
0
|
|
|
|
|
if ($ssl_opts) { |
253
|
0
|
|
|
|
|
|
$lwpUserAgentOptions{ssl_opts} = $ssl_opts; |
254
|
|
|
|
|
|
|
} |
255
|
|
|
|
|
|
|
|
256
|
0
|
|
|
|
|
|
my $ua = LWP::UserAgent->new( %lwpUserAgentOptions ); |
257
|
0
|
|
|
|
|
|
$ua->timeout($timeout); |
258
|
0
|
|
|
|
|
|
$self->{ua} = $ua; |
259
|
|
|
|
|
|
|
|
260
|
0
|
|
|
|
|
|
bless $self, $class; |
261
|
0
|
|
|
|
|
|
return $self |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
} |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
sub post { |
267
|
|
|
|
|
|
|
|
268
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
269
|
0
|
|
|
|
|
|
my $post_data; |
270
|
0
|
0
|
|
|
|
|
$post_data = pop |
271
|
|
|
|
|
|
|
if ref $_[-1]; |
272
|
0
|
0
|
|
|
|
|
my @path = @_ or return; # using || breaks this |
273
|
|
|
|
|
|
|
|
274
|
0
|
0
|
|
|
|
|
if ( $self->nodes ) { |
275
|
|
|
|
|
|
|
|
276
|
0
|
|
|
|
|
|
return $self->action( |
277
|
|
|
|
|
|
|
path => join( '/', @path ), |
278
|
|
|
|
|
|
|
method => 'POST', |
279
|
|
|
|
|
|
|
post_data => $post_data |
280
|
|
|
|
|
|
|
) |
281
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
} |
283
|
|
|
|
|
|
|
return |
284
|
0
|
|
|
|
|
|
} |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
sub put { |
288
|
|
|
|
|
|
|
|
289
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
290
|
0
|
|
|
|
|
|
my $post_data; |
291
|
0
|
0
|
|
|
|
|
$post_data = pop |
292
|
|
|
|
|
|
|
if ref $_[-1]; |
293
|
0
|
0
|
|
|
|
|
my @path = @_ or return; # using || breaks this |
294
|
|
|
|
|
|
|
|
295
|
0
|
0
|
|
|
|
|
if ( $self->nodes ) { |
296
|
|
|
|
|
|
|
|
297
|
0
|
|
|
|
|
|
return $self->action( |
298
|
|
|
|
|
|
|
path => join( '/', @path ), |
299
|
|
|
|
|
|
|
method => 'PUT', |
300
|
|
|
|
|
|
|
post_data => $post_data |
301
|
|
|
|
|
|
|
) |
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
} |
304
|
|
|
|
|
|
|
return |
305
|
0
|
|
|
|
|
|
} |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
sub url_prefix { |
310
|
|
|
|
|
|
|
|
311
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
# Prepare prefix for request |
314
|
|
|
|
|
|
|
my $url_prefix = sprintf( 'https://%s:%s', |
315
|
|
|
|
|
|
|
$self->{params}->{host}, |
316
|
0
|
|
|
|
|
|
$self->{params}->{port} ); |
317
|
|
|
|
|
|
|
|
318
|
0
|
|
|
|
|
|
return $url_prefix |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
} |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
1; |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
__END__ |