File Coverage

blib/lib/Net/Proxmox/VE.pm
Criterion Covered Total %
statement 36 241 14.9
branch 0 150 0.0
condition 0 50 0.0
subroutine 12 30 40.0
pod 13 13 100.0
total 61 484 12.6


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