| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
#!/bin/false |
|
2
|
|
|
|
|
|
|
# vim: softtabstop=4 tabstop=4 shiftwidth=4 ft=perl expandtab smarttab |
|
3
|
|
|
|
|
|
|
# PODNAME: Net::Proxmox::VE |
|
4
|
|
|
|
|
|
|
# ABSTRACT: Pure Perl API for Proxmox Virtual Environment |
|
5
|
|
|
|
|
|
|
|
|
6
|
1
|
|
|
1
|
|
172875
|
use strict; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
38
|
|
|
7
|
1
|
|
|
1
|
|
4
|
use warnings; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
142
|
|
|
8
|
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
package Net::Proxmox::VE; |
|
10
|
|
|
|
|
|
|
$Net::Proxmox::VE::VERSION = '0.44'; |
|
11
|
1
|
|
|
1
|
|
878
|
use HTTP::Headers (); |
|
|
1
|
|
|
|
|
4948
|
|
|
|
1
|
|
|
|
|
26
|
|
|
12
|
1
|
|
|
1
|
|
677
|
use HTTP::Request::Common (); |
|
|
1
|
|
|
|
|
19137
|
|
|
|
1
|
|
|
|
|
28
|
|
|
13
|
1
|
|
|
1
|
|
573
|
use JSON::MaybeXS qw(decode_json); |
|
|
1
|
|
|
|
|
9252
|
|
|
|
1
|
|
|
|
|
68
|
|
|
14
|
1
|
|
|
1
|
|
1191
|
use LWP::UserAgent (); |
|
|
1
|
|
|
|
|
32141
|
|
|
|
1
|
|
|
|
|
32
|
|
|
15
|
|
|
|
|
|
|
|
|
16
|
1
|
|
|
1
|
|
599
|
use Net::Proxmox::VE::Exception; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
61
|
|
|
17
|
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
# done |
|
19
|
1
|
|
|
1
|
|
513
|
use Net::Proxmox::VE::Access; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
102
|
|
|
20
|
1
|
|
|
1
|
|
577
|
use Net::Proxmox::VE::Cluster; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
115
|
|
|
21
|
1
|
|
|
1
|
|
468
|
use Net::Proxmox::VE::Pools; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
102
|
|
|
22
|
1
|
|
|
1
|
|
416
|
use Net::Proxmox::VE::Storage; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
57
|
|
|
23
|
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
# wip |
|
25
|
1
|
|
|
1
|
|
567
|
use Net::Proxmox::VE::Nodes; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
2730
|
|
|
26
|
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
my $API2_BASE_URL = 'https://%s:%s/api2/%s/'; |
|
28
|
|
|
|
|
|
|
my $DEFAULT_FORMAT = 'json'; |
|
29
|
|
|
|
|
|
|
my $DEFAULT_PORT = 8006; |
|
30
|
|
|
|
|
|
|
my $DEFAULT_REALM = 'pam'; |
|
31
|
|
|
|
|
|
|
my $DEFAULT_TIMEOUT = 10; |
|
32
|
|
|
|
|
|
|
my $DEFAULT_USERNAME = 'root'; |
|
33
|
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
sub _start_request { |
|
36
|
|
|
|
|
|
|
|
|
37
|
0
|
|
|
0
|
|
|
my ( $self, $params ) = @_; |
|
38
|
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
# Set up the request object |
|
40
|
0
|
|
|
|
|
|
my $request = HTTP::Request->new(); |
|
41
|
0
|
|
|
|
|
|
$request->method( $params->{method} ); |
|
42
|
0
|
0
|
|
|
|
|
if ( defined $self->{ticket} ) { |
|
43
|
|
|
|
|
|
|
$request->header( |
|
44
|
0
|
|
|
|
|
|
'Cookie' => 'PVEAuthCookie=' . $self->{ticket}->{ticket} ); |
|
45
|
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
# all methods other than GET require the prevention token |
|
47
|
|
|
|
|
|
|
# (i.e. anything that makes modification) |
|
48
|
0
|
0
|
|
|
|
|
if ( $params->{method} ne 'GET' ) { |
|
49
|
|
|
|
|
|
|
$request->header( |
|
50
|
|
|
|
|
|
|
'CSRFPreventionToken' => $self->{ticket}->{CSRFPreventionToken} |
|
51
|
0
|
|
|
|
|
|
); |
|
52
|
|
|
|
|
|
|
} |
|
53
|
|
|
|
|
|
|
} |
|
54
|
|
|
|
|
|
|
|
|
55
|
0
|
0
|
|
|
|
|
if ( defined $self->{pveapitoken} ) { |
|
56
|
|
|
|
|
|
|
$request->header( |
|
57
|
0
|
|
|
|
|
|
'Authorization' => 'PVEAPIToken=' . $self->{pveapitoken} ); |
|
58
|
|
|
|
|
|
|
} |
|
59
|
|
|
|
|
|
|
|
|
60
|
0
|
|
|
|
|
|
return $request; |
|
61
|
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
} |
|
63
|
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
sub action { |
|
65
|
|
|
|
|
|
|
|
|
66
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
67
|
0
|
|
|
|
|
|
my %params = @_; |
|
68
|
|
|
|
|
|
|
|
|
69
|
0
|
0
|
|
|
|
|
unless (%params) { |
|
70
|
0
|
|
|
|
|
|
Net::Proxmox::VE::Exception->throw( |
|
71
|
|
|
|
|
|
|
'action() requires a hash for params'); |
|
72
|
|
|
|
|
|
|
} |
|
73
|
|
|
|
|
|
|
Net::Proxmox::VE::Exception->throw('path param is required') |
|
74
|
0
|
0
|
|
|
|
|
unless $params{path}; |
|
75
|
|
|
|
|
|
|
|
|
76
|
0
|
|
0
|
|
|
|
$params{method} ||= 'GET'; |
|
77
|
0
|
|
0
|
|
|
|
$params{post_data} ||= {}; |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
# Check for a valid method |
|
80
|
|
|
|
|
|
|
Net::Proxmox::VE::Exception->throw( |
|
81
|
|
|
|
|
|
|
"invalid http method specified: $params{method}") |
|
82
|
0
|
0
|
|
|
|
|
unless $params{method} =~ m/^(GET|PUT|POST|DELETE)$/; |
|
83
|
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
# Strip prefixed / to path if present |
|
85
|
0
|
|
|
|
|
|
$params{path} =~ s{^/}{}; |
|
86
|
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
# Collapse duplicate slashes |
|
88
|
0
|
|
|
|
|
|
$params{path} =~ s{//+}{/}; |
|
89
|
|
|
|
|
|
|
|
|
90
|
0
|
0
|
0
|
|
|
|
unless ( $params{path} eq 'access/domains' |
|
91
|
|
|
|
|
|
|
or $self->check_login_ticket ) |
|
92
|
|
|
|
|
|
|
{ |
|
93
|
|
|
|
|
|
|
print "DEBUG: invalid login ticket\n" |
|
94
|
0
|
0
|
|
|
|
|
if $self->{params}->{debug}; |
|
95
|
0
|
0
|
|
|
|
|
return unless $self->login(); |
|
96
|
|
|
|
|
|
|
} |
|
97
|
|
|
|
|
|
|
|
|
98
|
0
|
|
|
|
|
|
my $url = $self->url_prefix . $params{path}; |
|
99
|
0
|
|
|
|
|
|
my $request = $self->_start_request( \%params ); |
|
100
|
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
# Grab useragent for convenience |
|
102
|
0
|
|
|
|
|
|
my $ua = $self->{ua}; |
|
103
|
|
|
|
|
|
|
|
|
104
|
0
|
|
|
|
|
|
my $response; |
|
105
|
0
|
0
|
|
|
|
|
if ( $params{method} =~ m/^(PUT|POST)$/ ) { |
|
106
|
0
|
|
|
|
|
|
$request->uri($url); |
|
107
|
|
|
|
|
|
|
my $content = join( '&', |
|
108
|
0
|
|
|
|
|
|
map { $_ . '=' . $params{post_data}->{$_} } |
|
109
|
0
|
|
|
|
|
|
sort keys %{ $params{post_data} } ); |
|
|
0
|
|
|
|
|
|
|
|
110
|
0
|
|
|
|
|
|
$request->content($content); |
|
111
|
0
|
|
|
|
|
|
$response = $ua->request($request); |
|
112
|
|
|
|
|
|
|
} |
|
113
|
0
|
0
|
|
|
|
|
if ( $params{method} =~ m/^(GET|DELETE)$/ ) { |
|
114
|
0
|
0
|
|
|
|
|
if ( %{ $params{post_data} } ) { |
|
|
0
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
my $qstring = join( '&', |
|
116
|
0
|
|
|
|
|
|
map { $_ . '=' . $params{post_data}->{$_} } |
|
117
|
0
|
|
|
|
|
|
sort keys %{ $params{post_data} } ); |
|
|
0
|
|
|
|
|
|
|
|
118
|
0
|
|
|
|
|
|
$request->uri("$url?$qstring"); |
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
else { |
|
121
|
0
|
|
|
|
|
|
$request->uri($url); |
|
122
|
|
|
|
|
|
|
} |
|
123
|
0
|
|
|
|
|
|
$response = $ua->request($request); |
|
124
|
|
|
|
|
|
|
} |
|
125
|
0
|
0
|
|
|
|
|
unless ( defined $response ) { |
|
126
|
|
|
|
|
|
|
# this shouldnt happen |
|
127
|
|
|
|
|
|
|
Net::Proxmox::VE::Exception->throw( |
|
128
|
0
|
|
|
|
|
|
'This shouldnt happen. Unknown method: ' . $params{method} ); |
|
129
|
|
|
|
|
|
|
} |
|
130
|
|
|
|
|
|
|
|
|
131
|
0
|
0
|
|
|
|
|
if ( $response->is_success ) { |
|
132
|
|
|
|
|
|
|
print 'DEBUG: successful request: ' . $request->as_string . "\n" |
|
133
|
0
|
0
|
|
|
|
|
if $self->{params}->{debug}; |
|
134
|
|
|
|
|
|
|
|
|
135
|
0
|
|
|
|
|
|
my $data = decode_json( $response->decoded_content ); |
|
136
|
|
|
|
|
|
|
|
|
137
|
0
|
0
|
0
|
|
|
|
if ( ref $data eq 'HASH' |
|
138
|
|
|
|
|
|
|
&& exists $data->{data} ) |
|
139
|
|
|
|
|
|
|
{ |
|
140
|
0
|
0
|
|
|
|
|
if ( ref $data->{data} eq 'ARRAY' ) { |
|
141
|
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
return wantarray |
|
143
|
0
|
|
|
|
|
|
? @{ $data->{data} } |
|
144
|
0
|
0
|
|
|
|
|
: $data->{data}; |
|
145
|
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
} |
|
147
|
|
|
|
|
|
|
|
|
148
|
0
|
|
|
|
|
|
return $data->{data}; |
|
149
|
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
} |
|
151
|
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
# just return true |
|
153
|
0
|
|
|
|
|
|
return 1; |
|
154
|
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
} |
|
156
|
|
|
|
|
|
|
else { |
|
157
|
0
|
|
|
|
|
|
Net::Proxmox::VE::Exception->throw( 'WARNING: request failed: ' |
|
158
|
|
|
|
|
|
|
. $request->as_string . "\n" |
|
159
|
|
|
|
|
|
|
. 'WARNING: response status: ' |
|
160
|
|
|
|
|
|
|
. $response->status_line |
|
161
|
|
|
|
|
|
|
. "\n" ); |
|
162
|
|
|
|
|
|
|
} |
|
163
|
0
|
|
|
|
|
|
return; |
|
164
|
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
} |
|
166
|
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
sub api_version { |
|
169
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
170
|
0
|
|
|
|
|
|
return $self->action( path => '/version', method => 'GET' ); |
|
171
|
|
|
|
|
|
|
} |
|
172
|
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
sub api_version_check { |
|
175
|
|
|
|
|
|
|
|
|
176
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
177
|
|
|
|
|
|
|
|
|
178
|
0
|
|
|
|
|
|
my $data = $self->api_version; |
|
179
|
|
|
|
|
|
|
|
|
180
|
0
|
0
|
0
|
|
|
|
if ( ref $data eq 'HASH' && $data->{version} ) { |
|
181
|
0
|
|
|
|
|
|
my ($version) = $data->{version} =~ m/^(\d+)/; |
|
182
|
0
|
0
|
|
|
|
|
return 1 if $version > 2.0; |
|
183
|
|
|
|
|
|
|
} |
|
184
|
|
|
|
|
|
|
|
|
185
|
0
|
|
|
|
|
|
return; |
|
186
|
|
|
|
|
|
|
} |
|
187
|
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
sub check_login_ticket { |
|
190
|
|
|
|
|
|
|
|
|
191
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
192
|
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
# API Tokens are always valid |
|
194
|
0
|
0
|
|
|
|
|
return 1 if $self->{pveapitoken}; |
|
195
|
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# Check we have a ticket loaded |
|
197
|
0
|
|
0
|
|
|
|
my $ticket = $self->{ticket} // return; |
|
198
|
0
|
0
|
|
|
|
|
return unless ref $ticket eq 'HASH'; |
|
199
|
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
# Check ticket appears valid |
|
201
|
|
|
|
|
|
|
my $is_valid = |
|
202
|
|
|
|
|
|
|
$ticket->{ticket} |
|
203
|
|
|
|
|
|
|
&& $ticket->{CSRFPreventionToken} |
|
204
|
|
|
|
|
|
|
&& $ticket->{username} eq "$self->{params}{username}\@$self->{params}{realm}" |
|
205
|
|
|
|
|
|
|
&& $self->{ticket_timestamp} |
|
206
|
0
|
|
0
|
|
|
|
&& ( $self->{ticket_timestamp} + $self->{ticket_life} ) > time(); |
|
207
|
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
# Clear invalid ticket |
|
209
|
0
|
0
|
|
|
|
|
$self->clear_login_ticket unless $is_valid; |
|
210
|
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
# Report if ticket seems valid |
|
212
|
0
|
|
|
|
|
|
return $is_valid; |
|
213
|
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
} |
|
215
|
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
sub clear_login_ticket { |
|
218
|
|
|
|
|
|
|
|
|
219
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
220
|
|
|
|
|
|
|
|
|
221
|
0
|
0
|
0
|
|
|
|
if ( $self->{ticket} or $self->{timestamp} ) { |
|
222
|
0
|
|
|
|
|
|
$self->{ticket} = undef; |
|
223
|
0
|
|
|
|
|
|
$self->{ticket_timestamp} = undef; |
|
224
|
0
|
|
|
|
|
|
return 1; |
|
225
|
|
|
|
|
|
|
} |
|
226
|
|
|
|
|
|
|
|
|
227
|
0
|
|
|
|
|
|
return; |
|
228
|
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
} |
|
230
|
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
sub debug { |
|
233
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
234
|
0
|
|
|
|
|
|
my $d = shift; |
|
235
|
|
|
|
|
|
|
|
|
236
|
0
|
0
|
|
|
|
|
if ($d) { |
|
|
|
0
|
|
|
|
|
|
|
237
|
0
|
|
|
|
|
|
$self->{params}->{debug} = 1; |
|
238
|
|
|
|
|
|
|
} |
|
239
|
|
|
|
|
|
|
elsif ( defined $d ) { |
|
240
|
0
|
|
|
|
|
|
$self->{params}->{debug} = 0; |
|
241
|
|
|
|
|
|
|
} |
|
242
|
|
|
|
|
|
|
|
|
243
|
0
|
0
|
|
|
|
|
return 1 if $self->{params}->{debug}; |
|
244
|
0
|
|
|
|
|
|
return; |
|
245
|
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
} |
|
247
|
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
sub delete { |
|
250
|
|
|
|
|
|
|
|
|
251
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
252
|
0
|
|
|
|
|
|
my $delete_data; |
|
253
|
0
|
0
|
|
|
|
|
$delete_data = pop |
|
254
|
|
|
|
|
|
|
if ref $_[-1]; |
|
255
|
0
|
0
|
|
|
|
|
my @path = @_ or return; # using || breaks this |
|
256
|
|
|
|
|
|
|
|
|
257
|
0
|
0
|
|
|
|
|
if ( $self->nodes ) { |
|
258
|
|
|
|
|
|
|
|
|
259
|
0
|
|
|
|
|
|
return $self->action( |
|
260
|
|
|
|
|
|
|
path => join( '/', @path ), |
|
261
|
|
|
|
|
|
|
method => 'DELETE', |
|
262
|
|
|
|
|
|
|
post_data => $delete_data |
|
263
|
|
|
|
|
|
|
); |
|
264
|
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
} |
|
266
|
0
|
|
|
|
|
|
return; |
|
267
|
|
|
|
|
|
|
} |
|
268
|
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
sub _get { |
|
271
|
|
|
|
|
|
|
|
|
272
|
0
|
|
|
0
|
|
|
my $self = shift; |
|
273
|
0
|
|
|
|
|
|
my $post_data = pop; |
|
274
|
0
|
|
|
|
|
|
my @path = @_; |
|
275
|
0
|
|
|
|
|
|
return $self->action( |
|
276
|
|
|
|
|
|
|
path => join( '/', @path ), |
|
277
|
|
|
|
|
|
|
method => 'GET', |
|
278
|
|
|
|
|
|
|
post_data => $post_data |
|
279
|
|
|
|
|
|
|
); |
|
280
|
|
|
|
|
|
|
} |
|
281
|
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
sub get { |
|
283
|
|
|
|
|
|
|
|
|
284
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
285
|
0
|
|
|
|
|
|
my $post_data; |
|
286
|
0
|
0
|
|
|
|
|
$post_data = pop |
|
287
|
|
|
|
|
|
|
if ref $_[-1]; |
|
288
|
0
|
0
|
|
|
|
|
my @path = @_ or return; # using || breaks this |
|
289
|
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
# Calling nodes method here would call get method itself and so on |
|
291
|
|
|
|
|
|
|
# Commented out to avoid an infinite loop |
|
292
|
0
|
0
|
|
|
|
|
if ( $self->nodes ) { |
|
293
|
0
|
|
|
|
|
|
return $self->_get( @path, $post_data ); |
|
294
|
|
|
|
|
|
|
} |
|
295
|
0
|
|
|
|
|
|
return; |
|
296
|
|
|
|
|
|
|
} |
|
297
|
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
sub _handle_tfa { |
|
300
|
|
|
|
|
|
|
|
|
301
|
0
|
|
|
0
|
|
|
my ($self, $challenge) = @_; |
|
302
|
|
|
|
|
|
|
|
|
303
|
0
|
|
0
|
|
|
|
my $totp = $self->{params}->{totp} // ''; |
|
304
|
|
|
|
|
|
|
# if $totp is a code ref then call it |
|
305
|
0
|
0
|
|
|
|
|
if (ref $totp eq 'CODE') { |
|
306
|
|
|
|
|
|
|
$totp = $totp->( |
|
307
|
|
|
|
|
|
|
username => $self->{params}->{username}, |
|
308
|
|
|
|
|
|
|
host => $self->{params}->{host}, |
|
309
|
|
|
|
|
|
|
realm => $self->{params}->{realm}, |
|
310
|
0
|
|
|
|
|
|
); |
|
311
|
|
|
|
|
|
|
} |
|
312
|
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
# Prepare login request w/ totp |
|
314
|
0
|
|
|
|
|
|
my $url = $self->url_prefix . 'access/ticket'; |
|
315
|
|
|
|
|
|
|
my $data = { |
|
316
|
|
|
|
|
|
|
'username' => $self->{params}->{username} . '@' |
|
317
|
|
|
|
|
|
|
. $self->{params}->{realm}, |
|
318
|
0
|
|
|
|
|
|
'password' => "totp:$totp", |
|
319
|
|
|
|
|
|
|
'tfa-challenge' => $challenge, |
|
320
|
|
|
|
|
|
|
}; |
|
321
|
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
# Perform login request w/ totp |
|
323
|
0
|
|
|
|
|
|
return $self->{ua}->post( $url, $data ); |
|
324
|
|
|
|
|
|
|
} |
|
325
|
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
sub login { |
|
327
|
|
|
|
|
|
|
|
|
328
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
329
|
|
|
|
|
|
|
|
|
330
|
0
|
0
|
|
|
|
|
if ( defined $self->{pveapitoken} ) { |
|
331
|
|
|
|
|
|
|
print "DEBUG: API Tokens are always logged in\n" |
|
332
|
0
|
0
|
|
|
|
|
if $self->{params}->{debug}; |
|
333
|
0
|
|
|
|
|
|
return 1; |
|
334
|
|
|
|
|
|
|
} |
|
335
|
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
# Prepare login request |
|
337
|
0
|
|
|
|
|
|
my $request_time = time(); |
|
338
|
0
|
|
|
|
|
|
my $url = $self->url_prefix . 'access/ticket'; |
|
339
|
|
|
|
|
|
|
my $request = { |
|
340
|
|
|
|
|
|
|
'username' => $self->{params}->{username} . '@' |
|
341
|
|
|
|
|
|
|
. $self->{params}->{realm}, |
|
342
|
|
|
|
|
|
|
'password' => $self->{params}->{password}, |
|
343
|
0
|
|
|
|
|
|
}; |
|
344
|
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
# Perform login request |
|
346
|
0
|
|
|
|
|
|
my $response = $self->{ua}->post( $url, $request ); |
|
347
|
|
|
|
|
|
|
|
|
348
|
0
|
0
|
|
|
|
|
if ( $response->is_success ) { |
|
349
|
0
|
|
|
|
|
|
my $login_ticket_data = decode_json( $response->decoded_content ); |
|
350
|
0
|
|
|
|
|
|
my $data = $login_ticket_data->{data}; |
|
351
|
|
|
|
|
|
|
# Take care of TFA if needed |
|
352
|
0
|
0
|
|
|
|
|
if ( $data->{NeedTFA} ) { |
|
353
|
0
|
0
|
|
|
|
|
unless ( defined $self->{totp} ) { |
|
354
|
|
|
|
|
|
|
print "DEBUG: totp required but not provided\n" |
|
355
|
0
|
0
|
|
|
|
|
if $self->{params}->{debug}; |
|
356
|
0
|
|
|
|
|
|
return; |
|
357
|
|
|
|
|
|
|
} |
|
358
|
0
|
|
|
|
|
|
$response = $self->_handle_tfa($data->{ticket}); |
|
359
|
0
|
0
|
|
|
|
|
if ($response->is_success) { |
|
360
|
|
|
|
|
|
|
print "DEBUG: tfa successful\n" |
|
361
|
0
|
0
|
|
|
|
|
if $self->{params}->{debug}; |
|
362
|
0
|
|
|
|
|
|
my $tfa_ticket_data = decode_json( $response->decoded_content ); |
|
363
|
0
|
|
|
|
|
|
$self->{ticket} = $tfa_ticket_data->{data}; |
|
364
|
|
|
|
|
|
|
} |
|
365
|
|
|
|
|
|
|
} |
|
366
|
|
|
|
|
|
|
else { |
|
367
|
0
|
|
|
|
|
|
$self->{ticket} = $data; |
|
368
|
|
|
|
|
|
|
} |
|
369
|
|
|
|
|
|
|
|
|
370
|
0
|
0
|
|
|
|
|
if ($data->{ticket}) { |
|
371
|
|
|
|
|
|
|
# We use the request time because the time to get the json ticket is undetermined. |
|
372
|
|
|
|
|
|
|
# It seems wiser to discard a ticket a few seconds before it expires rather than to incorrectly |
|
373
|
|
|
|
|
|
|
# continue using it after it has expired |
|
374
|
0
|
|
|
|
|
|
$self->{ticket_timestamp} = $request_time; |
|
375
|
|
|
|
|
|
|
print "DEBUG: login successful\n" |
|
376
|
0
|
0
|
|
|
|
|
if $self->{params}->{debug}; |
|
377
|
0
|
|
|
|
|
|
return 1; |
|
378
|
|
|
|
|
|
|
} |
|
379
|
|
|
|
|
|
|
} |
|
380
|
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
# If we get here then Login has failed |
|
382
|
0
|
0
|
|
|
|
|
if ( $self->{params}->{debug} ) { |
|
383
|
0
|
|
|
|
|
|
print "DEBUG: login not successful\n"; |
|
384
|
0
|
|
|
|
|
|
print "DEBUG: " . $response->status_line . "\n"; |
|
385
|
|
|
|
|
|
|
} |
|
386
|
|
|
|
|
|
|
|
|
387
|
0
|
|
|
|
|
|
return; |
|
388
|
|
|
|
|
|
|
} |
|
389
|
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
sub _load_auth { |
|
392
|
|
|
|
|
|
|
|
|
393
|
0
|
|
|
0
|
|
|
my ( $self, $params ) = @_; |
|
394
|
|
|
|
|
|
|
|
|
395
|
0
|
0
|
0
|
|
|
|
if ( ( $params->{password} or $params->{totp} ) |
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
396
|
|
|
|
|
|
|
and ( $params->{tokenid} or $params->{secret} ) ) |
|
397
|
|
|
|
|
|
|
{ |
|
398
|
0
|
|
|
|
|
|
Net::Proxmox::VE::Exception->throw( |
|
399
|
|
|
|
|
|
|
'Both password and API Token credentials provided.' |
|
400
|
|
|
|
|
|
|
. ' Please pick one authentication method' |
|
401
|
|
|
|
|
|
|
); |
|
402
|
|
|
|
|
|
|
} |
|
403
|
|
|
|
|
|
|
|
|
404
|
0
|
|
0
|
|
|
|
my $realm = delete $params->{realm} || $DEFAULT_REALM; |
|
405
|
0
|
|
0
|
|
|
|
my $username = delete $params->{username} || $DEFAULT_USERNAME; |
|
406
|
|
|
|
|
|
|
|
|
407
|
0
|
0
|
|
|
|
|
if ( $params->{password} ) { |
|
408
|
|
|
|
|
|
|
my $password = delete $params->{password} |
|
409
|
0
|
0
|
|
|
|
|
or Net::Proxmox::VE::Exception->throw('password param is required'); |
|
410
|
0
|
|
|
|
|
|
$self->{'params'}->{'password'} = $password; |
|
411
|
0
|
|
|
|
|
|
$self->{'params'}->{'realm'} = $realm; |
|
412
|
0
|
|
|
|
|
|
$self->{'params'}->{'username'} = $username; |
|
413
|
|
|
|
|
|
|
$self->{'params'}->{'totp'} = delete $params->{totp} |
|
414
|
0
|
0
|
|
|
|
|
if defined $params->{totp}; |
|
415
|
0
|
|
|
|
|
|
$self->{'ticket'} = undef; |
|
416
|
0
|
|
|
|
|
|
$self->{'ticket_timestamp'} = undef; |
|
417
|
0
|
|
|
|
|
|
$self->{'ticket_life'} = 7200; # 2 Hours |
|
418
|
0
|
|
|
|
|
|
return 1; |
|
419
|
|
|
|
|
|
|
} |
|
420
|
|
|
|
|
|
|
|
|
421
|
0
|
0
|
0
|
|
|
|
if ( $params->{tokenid} and $params->{secret} ) { |
|
422
|
0
|
|
|
|
|
|
my $tokenid = delete $params->{tokenid}; |
|
423
|
0
|
|
|
|
|
|
my $secret = delete $params->{secret}; |
|
424
|
0
|
|
|
|
|
|
$self->{'pveapitoken'} = sprintf( '%s@%s!%s=%s', $username, $realm, $tokenid, $secret ); |
|
425
|
0
|
|
|
|
|
|
return 1; |
|
426
|
|
|
|
|
|
|
} |
|
427
|
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
Net::Proxmox::VE::Exception->throw( |
|
429
|
0
|
|
|
|
|
|
'Incomplete authentication credentials provided.' |
|
430
|
|
|
|
|
|
|
. 'Either a password or tokenid and secret must be provided' ); |
|
431
|
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
} |
|
433
|
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
sub _create_ua { |
|
435
|
|
|
|
|
|
|
|
|
436
|
0
|
|
|
0
|
|
|
my ( $self, $params ) = @_; |
|
437
|
|
|
|
|
|
|
|
|
438
|
0
|
|
|
|
|
|
my $ssl_opts = delete $params->{ssl_opts}; |
|
439
|
0
|
|
|
|
|
|
my %lwpUserAgentOptions; |
|
440
|
0
|
0
|
|
|
|
|
if ($ssl_opts) { |
|
441
|
0
|
|
|
|
|
|
$lwpUserAgentOptions{ssl_opts} = $ssl_opts; |
|
442
|
|
|
|
|
|
|
} |
|
443
|
0
|
|
|
|
|
|
my $ua = LWP::UserAgent->new(%lwpUserAgentOptions); |
|
444
|
0
|
|
|
|
|
|
$ua->timeout( $self->{params}->{timeout} ); |
|
445
|
0
|
|
|
|
|
|
$self->{ua} = $ua; |
|
446
|
|
|
|
|
|
|
|
|
447
|
0
|
|
|
|
|
|
return 1; |
|
448
|
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
} |
|
450
|
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
sub new { |
|
452
|
|
|
|
|
|
|
|
|
453
|
0
|
|
|
0
|
1
|
|
my $c = shift; |
|
454
|
0
|
|
|
|
|
|
my @p = @_; |
|
455
|
0
|
|
0
|
|
|
|
my $class = ref($c) || $c; |
|
456
|
|
|
|
|
|
|
|
|
457
|
0
|
|
|
|
|
|
my %params; |
|
458
|
|
|
|
|
|
|
|
|
459
|
0
|
0
|
|
|
|
|
if ( scalar @p == 1 ) { |
|
|
|
0
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
|
|
461
|
0
|
0
|
|
|
|
|
Net::Proxmox::VE::Exception->throw('new() requires a hash for params') |
|
462
|
|
|
|
|
|
|
unless ref $p[0] eq 'HASH'; |
|
463
|
|
|
|
|
|
|
|
|
464
|
0
|
|
|
|
|
|
%params = %{ $p[0] }; |
|
|
0
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
} |
|
467
|
|
|
|
|
|
|
elsif ( scalar @p % 2 != 0 ) { |
|
468
|
0
|
|
|
|
|
|
Net::Proxmox::VE::Exception->throw( |
|
469
|
|
|
|
|
|
|
'new() called with an odd number of parameters'); |
|
470
|
|
|
|
|
|
|
|
|
471
|
|
|
|
|
|
|
} |
|
472
|
|
|
|
|
|
|
else { |
|
473
|
0
|
0
|
|
|
|
|
%params = @p |
|
474
|
|
|
|
|
|
|
or Net::Proxmox::VE::Exception->throw( |
|
475
|
|
|
|
|
|
|
'new() requires a hash for params'); |
|
476
|
|
|
|
|
|
|
} |
|
477
|
|
|
|
|
|
|
|
|
478
|
0
|
|
|
|
|
|
my $debug = delete $params{debug}; |
|
479
|
|
|
|
|
|
|
my $host = delete $params{host} |
|
480
|
0
|
0
|
|
|
|
|
or Net::Proxmox::VE::Exception->throw('host param is required'); |
|
481
|
0
|
|
0
|
|
|
|
my $port = delete $params{port} || $DEFAULT_PORT; |
|
482
|
0
|
|
0
|
|
|
|
my $timeout = delete $params{timeout} || $DEFAULT_TIMEOUT; |
|
483
|
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
my $self->{params} = { |
|
485
|
0
|
|
|
|
|
|
debug => $debug, |
|
486
|
|
|
|
|
|
|
host => $host, |
|
487
|
|
|
|
|
|
|
port => $port, |
|
488
|
|
|
|
|
|
|
timeout => $timeout, |
|
489
|
|
|
|
|
|
|
}; |
|
490
|
|
|
|
|
|
|
|
|
491
|
0
|
|
|
|
|
|
bless $self, $class; |
|
492
|
|
|
|
|
|
|
|
|
493
|
0
|
|
|
|
|
|
$self->_load_auth( \%params ); |
|
494
|
0
|
|
|
|
|
|
$self->_create_ua( \%params ); |
|
495
|
|
|
|
|
|
|
|
|
496
|
0
|
0
|
|
|
|
|
Net::Proxmox::VE::Exception->throw( |
|
497
|
|
|
|
|
|
|
'unknown parameters to new(): ' . join( ', ', keys %params ) ) |
|
498
|
|
|
|
|
|
|
if keys %params; |
|
499
|
|
|
|
|
|
|
|
|
500
|
0
|
|
|
|
|
|
return $self; |
|
501
|
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
} |
|
503
|
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
|
|
505
|
|
|
|
|
|
|
sub post { |
|
506
|
|
|
|
|
|
|
|
|
507
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
508
|
0
|
|
|
|
|
|
my $post_data; |
|
509
|
0
|
0
|
|
|
|
|
$post_data = pop |
|
510
|
|
|
|
|
|
|
if ref $_[-1]; |
|
511
|
0
|
0
|
|
|
|
|
my @path = @_ or return; # using || breaks this |
|
512
|
|
|
|
|
|
|
|
|
513
|
0
|
0
|
|
|
|
|
if ( $self->nodes ) { |
|
514
|
|
|
|
|
|
|
|
|
515
|
0
|
|
|
|
|
|
return $self->action( |
|
516
|
|
|
|
|
|
|
path => join( '/', @path ), |
|
517
|
|
|
|
|
|
|
method => 'POST', |
|
518
|
|
|
|
|
|
|
post_data => $post_data |
|
519
|
|
|
|
|
|
|
); |
|
520
|
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
} |
|
522
|
0
|
|
|
|
|
|
return; |
|
523
|
|
|
|
|
|
|
} |
|
524
|
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
sub put { |
|
527
|
|
|
|
|
|
|
|
|
528
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
529
|
0
|
|
|
|
|
|
my $post_data; |
|
530
|
0
|
0
|
|
|
|
|
$post_data = pop |
|
531
|
|
|
|
|
|
|
if ref $_[-1]; |
|
532
|
0
|
0
|
|
|
|
|
my @path = @_ or return; # using || breaks this |
|
533
|
|
|
|
|
|
|
|
|
534
|
0
|
0
|
|
|
|
|
if ( $self->nodes ) { |
|
535
|
|
|
|
|
|
|
|
|
536
|
0
|
|
|
|
|
|
return $self->action( |
|
537
|
|
|
|
|
|
|
path => join( '/', @path ), |
|
538
|
|
|
|
|
|
|
method => 'PUT', |
|
539
|
|
|
|
|
|
|
post_data => $post_data |
|
540
|
|
|
|
|
|
|
); |
|
541
|
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
} |
|
543
|
0
|
|
|
|
|
|
return; |
|
544
|
|
|
|
|
|
|
} |
|
545
|
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
sub url_prefix { |
|
548
|
|
|
|
|
|
|
|
|
549
|
0
|
0
|
|
0
|
1
|
|
my $self = shift or return; |
|
550
|
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
# Prepare prefix for request |
|
552
|
|
|
|
|
|
|
my $url_prefix = sprintf( $API2_BASE_URL, |
|
553
|
|
|
|
|
|
|
$self->{params}->{host}, |
|
554
|
|
|
|
|
|
|
$self->{params}->{port}, |
|
555
|
0
|
|
|
|
|
|
$DEFAULT_FORMAT); |
|
556
|
|
|
|
|
|
|
|
|
557
|
0
|
|
|
|
|
|
return $url_prefix; |
|
558
|
|
|
|
|
|
|
|
|
559
|
|
|
|
|
|
|
} |
|
560
|
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
1; |
|
563
|
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
__END__ |
|
565
|
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
=pod |
|
567
|
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
=encoding UTF-8 |
|
569
|
|
|
|
|
|
|
|
|
570
|
|
|
|
|
|
|
=head1 NAME |
|
571
|
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
Net::Proxmox::VE - Pure Perl API for Proxmox Virtual Environment |
|
573
|
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
=head1 VERSION |
|
575
|
|
|
|
|
|
|
|
|
576
|
|
|
|
|
|
|
version 0.44 |
|
577
|
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
579
|
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
use Net::Proxmox::VE; |
|
581
|
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
# User+Password Authentication |
|
583
|
|
|
|
|
|
|
%args = ( |
|
584
|
|
|
|
|
|
|
host => 'proxmox.local.domain', |
|
585
|
|
|
|
|
|
|
password => 'barpassword', |
|
586
|
|
|
|
|
|
|
username => 'root', # optional |
|
587
|
|
|
|
|
|
|
port => 8006, # optional |
|
588
|
|
|
|
|
|
|
totp => 123456, # optional |
|
589
|
|
|
|
|
|
|
realm => 'pam', # optional |
|
590
|
|
|
|
|
|
|
); |
|
591
|
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
$host = Net::Proxmox::VE->new(%args); |
|
593
|
|
|
|
|
|
|
|
|
594
|
|
|
|
|
|
|
$host->login() or die ('Couldn\'t log in to proxmox host'); |
|
595
|
|
|
|
|
|
|
|
|
596
|
|
|
|
|
|
|
# API Token Authentication |
|
597
|
|
|
|
|
|
|
%args = ( |
|
598
|
|
|
|
|
|
|
host => 'proxmox.local.domain', |
|
599
|
|
|
|
|
|
|
tokenid => 'example', |
|
600
|
|
|
|
|
|
|
secret => 'uuid', |
|
601
|
|
|
|
|
|
|
username => 'root', # optional |
|
602
|
|
|
|
|
|
|
port => 8006, # optional |
|
603
|
|
|
|
|
|
|
realm => 'pam', # optional |
|
604
|
|
|
|
|
|
|
); |
|
605
|
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
$host = Net::Proxmox::VE->new(%args); |
|
607
|
|
|
|
|
|
|
|
|
608
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
609
|
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
This Class provides a framework for talking to Proxmox VE REST API instances including ticket headers required |
|
611
|
|
|
|
|
|
|
for authentication. You can use just the get/delete/put/post abstraction layer or use the api function methods. |
|
612
|
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
Object representations of the Proxmox VE REST API are included in seperate modules. |
|
614
|
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
You can use either User+Password or API Tokens for authentication. See also L<https://pve.proxmox.com/wiki/User_Management> |
|
616
|
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
There is currently no support for 2FA (pull requests welcome). |
|
618
|
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
=head1 WARNING |
|
620
|
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
We are still moving things around and trying to come up with something |
|
622
|
|
|
|
|
|
|
that makes sense. We havent yet implemented all the API functions, |
|
623
|
|
|
|
|
|
|
so far we only have a basic internal abstraction of the REST interface |
|
624
|
|
|
|
|
|
|
and a few modules for each function tree within the API. |
|
625
|
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
Any enhancements are greatly appreciated ! (use github, link below) |
|
627
|
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
Please dont be offended if we refactor and rework submissions. |
|
629
|
|
|
|
|
|
|
Perltidy with default settings is prefered style. |
|
630
|
|
|
|
|
|
|
|
|
631
|
|
|
|
|
|
|
Oh, our tests are all against a running server. Care to help make them better? |
|
632
|
|
|
|
|
|
|
|
|
633
|
|
|
|
|
|
|
=head1 METHODS |
|
634
|
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
=head2 action |
|
636
|
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
This calls raw actions against your proxmox server. |
|
638
|
|
|
|
|
|
|
Ideally you don't use this directly. |
|
639
|
|
|
|
|
|
|
|
|
640
|
|
|
|
|
|
|
=head2 api_version |
|
641
|
|
|
|
|
|
|
|
|
642
|
|
|
|
|
|
|
Returns the API version of the proxmox server we are talking to, |
|
643
|
|
|
|
|
|
|
including some parts of the global datacenter config. |
|
644
|
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
No arguments are available. |
|
646
|
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
A hash will be returned which will include the following: |
|
648
|
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
=over 4 |
|
650
|
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
=item release |
|
652
|
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
String. The current Proxmox VE point release in `x.y` format. |
|
654
|
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
=item repoid |
|
656
|
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
String. The short git revision from which this version was build. |
|
658
|
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
=item version |
|
660
|
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
String. The full pve-manager package version of this node. |
|
662
|
|
|
|
|
|
|
|
|
663
|
|
|
|
|
|
|
=item console |
|
664
|
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
Enum. The default console viewer to use. Optional. |
|
666
|
|
|
|
|
|
|
|
|
667
|
|
|
|
|
|
|
Available values: applet, vv, html5, xtermjs |
|
668
|
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
=back |
|
670
|
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
=head2 api_version_check |
|
672
|
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
Checks that the api we are talking to is at least version 2.0 |
|
674
|
|
|
|
|
|
|
|
|
675
|
|
|
|
|
|
|
Returns true if the api version is at least 2.0 (perl style true or false) |
|
676
|
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
=head2 check_login_ticket |
|
678
|
|
|
|
|
|
|
|
|
679
|
|
|
|
|
|
|
Verifies if the objects login ticket is valid and not expired |
|
680
|
|
|
|
|
|
|
|
|
681
|
|
|
|
|
|
|
Returns true if valid |
|
682
|
|
|
|
|
|
|
Returns false and clears the the login ticket details inside the object if invalid |
|
683
|
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
=head2 clear_login_ticket |
|
685
|
|
|
|
|
|
|
|
|
686
|
|
|
|
|
|
|
Clears the login ticket inside the object |
|
687
|
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
=head2 debug |
|
689
|
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
Has a single optional argument of 1 or 0 representing enable or disable debugging. |
|
691
|
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
Undef (ie no argument) leaves the debug status untouched, making this method call simply a query. |
|
693
|
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
Returns the resultant debug status (perl style true or false) |
|
695
|
|
|
|
|
|
|
|
|
696
|
|
|
|
|
|
|
=head2 delete |
|
697
|
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
An action helper method that takes a path list and delete data as an argument and returns the |
|
699
|
|
|
|
|
|
|
value of action() with the DELETE method |
|
700
|
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
$obj->delete( @path ); |
|
702
|
|
|
|
|
|
|
$obj->delete( @path, \%delete_data ); |
|
703
|
|
|
|
|
|
|
|
|
704
|
|
|
|
|
|
|
=head2 get |
|
705
|
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
An action helper method that just takes a path as an argument and returns the |
|
707
|
|
|
|
|
|
|
value of action with the GET method |
|
708
|
|
|
|
|
|
|
|
|
709
|
|
|
|
|
|
|
=head2 login |
|
710
|
|
|
|
|
|
|
|
|
711
|
|
|
|
|
|
|
Initiates the login to the PVE Server using JSON API, and potentially obtains an Access Ticket. |
|
712
|
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
Returns true if successful |
|
714
|
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
=head2 new |
|
716
|
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
Creates the Net::Proxmox::VE object and returns it. |
|
718
|
|
|
|
|
|
|
|
|
719
|
|
|
|
|
|
|
Examples... |
|
720
|
|
|
|
|
|
|
|
|
721
|
|
|
|
|
|
|
my $obj = Net::Proxmox::VE->new(%args); |
|
722
|
|
|
|
|
|
|
my $obj = Net::Proxmox::VE->new(\%args); |
|
723
|
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
Authentication arguments are... |
|
725
|
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
=over 4 |
|
727
|
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
=item I<username> |
|
729
|
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
User name used for authentication. Defaults to 'root', optional. |
|
731
|
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
=item I<password> |
|
733
|
|
|
|
|
|
|
|
|
734
|
|
|
|
|
|
|
Pass word user for authentication. Either use this password field or I<tokenid> and I<secret>. |
|
735
|
|
|
|
|
|
|
|
|
736
|
|
|
|
|
|
|
=item I<totp> |
|
737
|
|
|
|
|
|
|
|
|
738
|
|
|
|
|
|
|
Either the totp code or a sub ref to code that will return the totop code. |
|
739
|
|
|
|
|
|
|
|
|
740
|
|
|
|
|
|
|
totp => '12345', |
|
741
|
|
|
|
|
|
|
totp => sub { my %args = @_; return '12345' }, |
|
742
|
|
|
|
|
|
|
|
|
743
|
|
|
|
|
|
|
If a subref is provided, the %args will include the keys I<username>, I<realm>, and I<host> with corresponding |
|
744
|
|
|
|
|
|
|
values. These may optionally be used to help determine the topt. |
|
745
|
|
|
|
|
|
|
|
|
746
|
|
|
|
|
|
|
Only valid with I<username> and I<password> parameters. |
|
747
|
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
=item I<tokenid> |
|
749
|
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
The tokenid of the API keys being used. Optional. |
|
751
|
|
|
|
|
|
|
|
|
752
|
|
|
|
|
|
|
=item I<secret> |
|
753
|
|
|
|
|
|
|
|
|
754
|
|
|
|
|
|
|
The secret of the API keys being used. Optional, although required when a I<tokenid> is provided. |
|
755
|
|
|
|
|
|
|
|
|
756
|
|
|
|
|
|
|
This is distinct from the I<password> field. |
|
757
|
|
|
|
|
|
|
|
|
758
|
|
|
|
|
|
|
=back |
|
759
|
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
Other arguments are... |
|
761
|
|
|
|
|
|
|
|
|
762
|
|
|
|
|
|
|
=over 4 |
|
763
|
|
|
|
|
|
|
|
|
764
|
|
|
|
|
|
|
=item I<host> |
|
765
|
|
|
|
|
|
|
|
|
766
|
|
|
|
|
|
|
Proxmox host instance to interact with. Required so no default. |
|
767
|
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
=item I<port> |
|
769
|
|
|
|
|
|
|
|
|
770
|
|
|
|
|
|
|
TCP port number used to by the Proxmox host instance. Defaults to 8006, optional. |
|
771
|
|
|
|
|
|
|
|
|
772
|
|
|
|
|
|
|
=item I<realm> |
|
773
|
|
|
|
|
|
|
|
|
774
|
|
|
|
|
|
|
Authentication realm to request against. Defaults to 'pam' (local auth), optional. |
|
775
|
|
|
|
|
|
|
|
|
776
|
|
|
|
|
|
|
=item I<ssl_opts> |
|
777
|
|
|
|
|
|
|
|
|
778
|
|
|
|
|
|
|
If you're using a self-signed certificate, SSL verification is going to fail, and we need to tell C<IO::Socket::SSL> not to attempt certificate verification. |
|
779
|
|
|
|
|
|
|
|
|
780
|
|
|
|
|
|
|
This option is passed on as C<ssl_opts> options to C<LWP::UserAgent-E<gt>new()>, ultimately for C<IO::Socket::SSL>. |
|
781
|
|
|
|
|
|
|
|
|
782
|
|
|
|
|
|
|
Using it like this, causes C<LWP::UserAgent> and C<IO::Socket::SSL> not to attempt SSL verification: |
|
783
|
|
|
|
|
|
|
|
|
784
|
|
|
|
|
|
|
use IO::Socket::SSL qw(SSL_VERIFY_NONE); |
|
785
|
|
|
|
|
|
|
.. |
|
786
|
|
|
|
|
|
|
%args = ( |
|
787
|
|
|
|
|
|
|
... |
|
788
|
|
|
|
|
|
|
ssl_opts => { |
|
789
|
|
|
|
|
|
|
SSL_verify_mode => SSL_VERIFY_NONE, |
|
790
|
|
|
|
|
|
|
verify_hostname => 0 |
|
791
|
|
|
|
|
|
|
}, |
|
792
|
|
|
|
|
|
|
... |
|
793
|
|
|
|
|
|
|
); |
|
794
|
|
|
|
|
|
|
my $proxmox = Net::Proxmox::VE->new(%args); |
|
795
|
|
|
|
|
|
|
|
|
796
|
|
|
|
|
|
|
Your connection will work now, but B<beware: you are now susceptible to a man-in-the-middle attack>. |
|
797
|
|
|
|
|
|
|
|
|
798
|
|
|
|
|
|
|
=item I<debug> |
|
799
|
|
|
|
|
|
|
|
|
800
|
|
|
|
|
|
|
Enabling debugging of this API (not related to proxmox debugging in any way). Defaults to false, optional. |
|
801
|
|
|
|
|
|
|
|
|
802
|
|
|
|
|
|
|
=back |
|
803
|
|
|
|
|
|
|
|
|
804
|
|
|
|
|
|
|
=head2 post |
|
805
|
|
|
|
|
|
|
|
|
806
|
|
|
|
|
|
|
An action helper method that takes two parameters: $path, \%post_data |
|
807
|
|
|
|
|
|
|
$path to post to, hash ref to %post_data |
|
808
|
|
|
|
|
|
|
|
|
809
|
|
|
|
|
|
|
You are returned what action() with the POST method returns |
|
810
|
|
|
|
|
|
|
|
|
811
|
|
|
|
|
|
|
=head2 put |
|
812
|
|
|
|
|
|
|
|
|
813
|
|
|
|
|
|
|
An action helper method that takes two parameters: |
|
814
|
|
|
|
|
|
|
$path, hash ref to \%put_data |
|
815
|
|
|
|
|
|
|
|
|
816
|
|
|
|
|
|
|
You are returned what action() with the PUT method returns |
|
817
|
|
|
|
|
|
|
|
|
818
|
|
|
|
|
|
|
=head2 url_prefix |
|
819
|
|
|
|
|
|
|
|
|
820
|
|
|
|
|
|
|
Returns the url prefix used in the rest api calls |
|
821
|
|
|
|
|
|
|
|
|
822
|
|
|
|
|
|
|
=head1 PVE VERSIONS SUPPORT |
|
823
|
|
|
|
|
|
|
|
|
824
|
|
|
|
|
|
|
Firstly, there isn't currently any handling of different versions of the API. |
|
825
|
|
|
|
|
|
|
|
|
826
|
|
|
|
|
|
|
Secondly, Proxmox API reference documentation is also, frustratingly, published only alongside the current release. This makes it difficult to support older versions of the API or different versions of the API concurrently. |
|
827
|
|
|
|
|
|
|
|
|
828
|
|
|
|
|
|
|
Fortunately the API is relatively stable. |
|
829
|
|
|
|
|
|
|
|
|
830
|
|
|
|
|
|
|
Based on the above the bug reporting policy is as follows: |
|
831
|
|
|
|
|
|
|
|
|
832
|
|
|
|
|
|
|
=over 2 |
|
833
|
|
|
|
|
|
|
|
|
834
|
|
|
|
|
|
|
=item A function in this module doesn't work against the current published API? This a bug and hope to fix it. Pull requests welcome. |
|
835
|
|
|
|
|
|
|
|
|
836
|
|
|
|
|
|
|
=item A function in this module doesn't exist in the current published API? Pull requests welcomes and promptly merged. |
|
837
|
|
|
|
|
|
|
|
|
838
|
|
|
|
|
|
|
=item A function in this module doesn't work against a previous version of the API? A note will be made in the pod only. |
|
839
|
|
|
|
|
|
|
|
|
840
|
|
|
|
|
|
|
=item A function in this module doesn't exist against a previous version of the API? Pull requests will be merged on a case per case basis. |
|
841
|
|
|
|
|
|
|
|
|
842
|
|
|
|
|
|
|
=back |
|
843
|
|
|
|
|
|
|
|
|
844
|
|
|
|
|
|
|
As such breaking changes may be made to this module to support the current API when necessary. |
|
845
|
|
|
|
|
|
|
|
|
846
|
|
|
|
|
|
|
=head1 DESIGN NOTE |
|
847
|
|
|
|
|
|
|
|
|
848
|
|
|
|
|
|
|
This API would be far nicer if it returned nice objects representing different aspects of the system. |
|
849
|
|
|
|
|
|
|
Such an arrangement would be far better than how this module is currently layed out. It might also be |
|
850
|
|
|
|
|
|
|
less repetitive code. |
|
851
|
|
|
|
|
|
|
|
|
852
|
|
|
|
|
|
|
=head1 SEE ALSO |
|
853
|
|
|
|
|
|
|
|
|
854
|
|
|
|
|
|
|
=over 4 |
|
855
|
|
|
|
|
|
|
|
|
856
|
|
|
|
|
|
|
=item Proxmox Website |
|
857
|
|
|
|
|
|
|
|
|
858
|
|
|
|
|
|
|
http://www.proxmox.com |
|
859
|
|
|
|
|
|
|
|
|
860
|
|
|
|
|
|
|
=item API Reference |
|
861
|
|
|
|
|
|
|
|
|
862
|
|
|
|
|
|
|
More details on the API can be found at L<http://pve.proxmox.com/wiki/Proxmox_VE_API> and |
|
863
|
|
|
|
|
|
|
L<http://pve.proxmox.com/pve-docs/api-viewer/index.html> |
|
864
|
|
|
|
|
|
|
|
|
865
|
|
|
|
|
|
|
=back |
|
866
|
|
|
|
|
|
|
|
|
867
|
|
|
|
|
|
|
=head1 AUTHOR |
|
868
|
|
|
|
|
|
|
|
|
869
|
|
|
|
|
|
|
Dean Hamstead <dean@fragfest.com.au> |
|
870
|
|
|
|
|
|
|
|
|
871
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
|
872
|
|
|
|
|
|
|
|
|
873
|
|
|
|
|
|
|
This software is Copyright (c) 2026 by Dean Hamstead. |
|
874
|
|
|
|
|
|
|
|
|
875
|
|
|
|
|
|
|
This is free software, licensed under: |
|
876
|
|
|
|
|
|
|
|
|
877
|
|
|
|
|
|
|
The MIT (X11) License |
|
878
|
|
|
|
|
|
|
|
|
879
|
|
|
|
|
|
|
=cut |