File Coverage

blib/lib/Device/PaloAlto/Firewall.pm
Criterion Covered Total %
statement 33 35 94.2
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 45 47 95.7


line stmt bran cond sub pod time code
1             package Device::PaloAlto::Firewall;
2              
3 4     4   241600 use 5.006;
  4         17  
4 4     4   22 use strict;
  4         7  
  4         79  
5 4     4   17 use warnings;
  4         11  
  4         179  
6              
7             our $VERSION = '0.081'; # VERSION - generated by DZP::OurPkgVersion
8              
9 4     4   1254 use Device::PaloAlto::Firewall::Test;
  4         11  
  4         151  
10              
11 4     4   33 use Moose;
  4         7  
  4         23  
12 4     4   16832 use Modern::Perl;
  4         9  
  4         33  
13 4     4   2740 use LWP::UserAgent;
  4         91711  
  4         198  
14 4     4   54 use HTTP::Request;
  4         9  
  4         139  
15 4     4   31 use Carp;
  4         9  
  4         364  
16 4     4   34 use Params::Validate qw(:all);
  4         13  
  4         801  
17 4     4   26 use URI;
  4         10  
  4         83  
18 4     4   6191 use XML::Twig;
  0            
  0            
19             use Memoize qw{memoize unmemoize};
20              
21             use Data::Dumper;
22              
23             =head1 NAME
24              
25             Device::PaloAlto::Firewall - Interact with the Palo Alto firewall API
26              
27             =head1 VERSION
28              
29             version 0.081
30              
31             =cut
32              
33             =head1 SYNOPSIS
34              
35             Device::PaloAlto::Firewall provides interfaces to B<retrieve> information from a Palo Alto firewall.
36              
37             my $firewall = Device::PaloAlto::Firewall->new(uri => 'http://localhost.localdomain', username => 'admin', password => 'complex_password')
38              
39             my $environ = $firewall->environmentals();
40             my $interfaces = $firewall->interfaces();
41              
42             A key point is that that methods only retrieve information. There are no methods within this module to modify or commit configuration.
43              
44             The methods return either an ARRAYREF or HASHREF, and the data structure returned is largely unchanged from what the firewall returns. Some general cleaning may
45             be have done, and some changes occur when the XML from the firewall is parsed and turned into a Perl structure.
46              
47             For brevity, examples of the return values aren't documented within this document but in a separate L<Device::PaloAlto:Firewall::Return> page.
48              
49             =head1 CONSTRUCTOR
50              
51             The C<new()> constructor takes the following arguments:
52              
53             =over 4
54              
55             =item * C<uri> - A HTTP or HTTPS URI to the firewall.
56              
57             =item * C<username> - a username to authenticate to the device.
58              
59             =item * C<password> - a password for the username.
60              
61             =back
62              
63             =cut
64              
65             has 'user_agent' => ( is => 'ro', isa => 'LWP::UserAgent', init_arg => undef, default => sub { LWP::UserAgent->new } );
66             has 'http_request' => ( is => 'rw', isa => 'HTTP::Request', init_arg => undef, default => sub { HTTP::Request->new } );
67             has 'uri' => ( is => 'ro', writer => '_uri', required => 1);
68              
69             has 'username' => ( is => 'ro', isa => 'Str', required => 1 );
70             has 'password' => ( is => 'ro', isa => 'Str', required => 1 );
71             has '_api_key' => ( is => 'rw', init_arg => undef, default => undef );
72              
73             has 'debug' => ( is => 'rw', isa => 'Bool', default => 0);
74              
75              
76             sub BUILD {
77             my $self = shift;
78            
79             #URI string gets changed into a URI object
80             my $uri_obj = URI->new($self->uri);
81             if (!$uri_obj->has_recognized_scheme) {
82             croak "Unrecognised URI passed to constructor";
83             }
84              
85             #Set the path to API located
86             $uri_obj->path("/api/");
87             $self->_uri( $uri_obj );
88              
89             # Request method is always GET
90             $self->http_request->method( 'GET' );
91              
92             # Lower the timeout for the user agent to 5 seconds
93             $self->user_agent->timeout( 5 );
94              
95             return;
96             }
97              
98             =head1 METHODS
99              
100             =head2 META
101              
102             These methods affect the way requests are made to the firewalls.
103              
104             =head3 authenticate
105              
106             Manually authenticates to the firewall.
107              
108             =cut
109              
110             sub authenticate {
111             my $self = shift;
112              
113             return 1 if $self->_api_key;
114              
115             $self->uri->query( "type=keygen&user=".$self->username."&password=".$self->password );
116             $self->http_request->uri( $self->uri->as_string );
117            
118             # Get the HTTP response and check it for errors
119             my $http_response = $self->_send_http_request();
120             return if !$self->_check_http_response($http_response);
121              
122             # Get the PA response (XML to a Perl Structure) from the body and check for errors
123             my $api_key_response = $self->_get_pa_response($http_response);
124             return if !$self->_check_pa_response($api_key_response);
125              
126             if (!$api_key_response or !$api_key_response->{result} or !$api_key_response->{result}->{key}) {
127             carp "API key error: no valid key in response";
128             return;
129             }
130              
131             $self->_api_key( $api_key_response->{result}->{key} );
132              
133             return 1;
134             }
135              
136              
137             =head3 verify_hostname
138              
139             Enables/disables the verification of the peer certificate and hostname if 'https' is used for API calls. By default TLS peer verification is B<enabled>.
140              
141             $fw->verify_hostname(1); Enable TLS peer verification
142             $fw->verify_hostname(0); Disable TLS verification
143              
144             =cut
145              
146             sub verify_hostname {
147             my $self = shift;
148             my $verify_bool = shift;
149             my $verify_mode = $verify_bool ?
150             0x01 # 'SSL_VERIFY_PEER'
151             :
152             0x00; # 'SSL_VERIFY_NONE'
153              
154             $self->user_agent->ssl_opts( verify_hostname => $verify_bool, SSL_verify_mode => $verify_mode );
155              
156             return;
157             }
158              
159             =head3 optimise
160              
161             Enables/disables the local caching of requests and responses to the firewall. This is disabled by default.
162              
163             $fw->optimise(1); # Enable optimisation
164             my $system_info = $fw->system_info(); # API call to retrieve interface information
165             $system_info = $fw->system_info(); # Information retrieved from local cache
166              
167             The first call to C<system_info()> will make an API call to the firewall and cache the result. The second request will retrieve the response from the local cache without making an API call.
168             Under the covers it uses C<Memoize> to cache the API request call. This means that each function & arguments receive their own cache. For example:
169            
170             $fw->optimise(1);
171             my $default_vr bgp_peers = $fw->bgp_peers(vrouter => 'default');
172             my $other_vr_bgp_peers = $fw->bgp_peers(vrouter => 'other');
173              
174             Both of these methods would make an API call to the firewall as the arguments differ.
175              
176             =cut
177              
178             sub optimise {
179             my $self = shift;
180             my $bool = shift;
181            
182             if ($bool) {
183             memoize('Device::PaloAlto::Firewall::_send_request');
184             } else {
185             unmemoize('Device::PaloAlto::Firewall::_send_request');
186             }
187              
188             return;
189             }
190              
191             =head3 tester
192              
193             Retrieves a C<Device::PaloAlto::Firewall::Test> object for this firewall.
194              
195             use Test::More;
196             my $test = Device::PaloAlto::Firewall->new(uri => 'http://remote_pa.domain', username => 'test', password => 'test')->tester();
197              
198             ok( $test->interfaces_up(interfaces => ['ethernet1/1']) );
199              
200             For more information, see the L<Device::PaloAlto::Firewall::Test> documentation.
201            
202             =cut
203              
204             sub tester {
205             my $self = shift;
206              
207             return Device::PaloAlto::Firewall::Test->new(firewall => $self);
208             }
209              
210             =head2 PLATFORM
211              
212             These methods retrieve information on the firewall platform.
213              
214             =head3 system_info
215              
216             Returns system information from the firewall.
217              
218             my $system_info = $fw->system_info();
219             say "Current Time on Firewall: $system_info->{time}";
220              
221             =cut
222              
223             sub system_info {
224             my $self = shift;
225             my $system_info = $self->_send_request(command => "<show><system><info></info></system></show>");
226              
227             return if !defined $system_info;
228              
229             return $system_info->{system};
230             }
231              
232              
233              
234             =head3 environmentals
235              
236             Returns information on the system environmentals. This includes the fantray and fans, power supplies and power, temperature. B<Note:> virtual machines don't have any environmental information and won't return any information.
237              
238             =cut
239              
240             sub environmentals {
241             my $self = shift;
242              
243             my $environs = $self->_send_request(command => "<show><system><environmentals></environmentals></system></show>");
244              
245             return if !defined $environs;
246              
247             # Our structure comes back looking like
248             # { $property => { $slot => { 'entry' => [ { %info } ] } } }
249             #
250             # We modify the structure to remove the redundant 'entry' and make sure
251             # Single and multiple '%info' hashes are in an arrayref
252             # { $property => { $slot => [ { %info } ] } }
253            
254             for my $property (values %{ $environs }) {
255             for my $slot (values %{ $property }) {
256             $slot = $slot->{entry};
257             }
258             }
259              
260              
261             return $environs;
262             }
263              
264              
265              
266             =head3 high_availability
267              
268             Retrieves information on the high availability status of the firewall.
269              
270             =cut
271              
272             sub high_availability {
273             my $self = shift;
274             return $self->_send_request(command => "<show><high-availability><all></all></high-availability></show>");
275             }
276              
277              
278              
279             =head2 NETWORK
280              
281             These methods retrieve network information from the firewall.
282              
283             =head3 interfaces
284              
285             Retrieves interface information.
286              
287             =cut
288              
289             sub interfaces {
290             my $self = shift;
291             my $interfaces = $self->_send_request(command => "<show><interface>all</interface></show>");
292             return $interfaces;
293             }
294              
295              
296              
297             =head3 interface_counters_logical
298              
299             Retrieves information on the logical interface counters.
300              
301             =cut
302              
303             sub interface_counters_logical {
304             my $self = shift;
305             my $counters = $self->_send_request(command => '<show><counter><interface>all</interface></counter></show>');
306              
307             return if !defined $counters;
308              
309             my $ret = $counters->{ifnet}->{ifnet}->{entry};
310              
311             return [] if !defined $ret;
312              
313             return $ret;
314             }
315              
316              
317              
318             =head3 routing_table
319              
320             Retrives information on the routing table for a particular virtual router. If no C<vrouter> argument is specified it retrieves the 'default' vrouter's routing table.
321              
322             my $default_vr_table = $fw->routing_table();
323             my $corp_vr_table = $fw->routing_table(vrouter => 'corp');
324              
325             =cut
326              
327             sub routing_table {
328             my $self = shift;
329             my %args = validate(@_,
330             {
331             vrouter => { default => 'default', type => SCALAR | UNDEF },
332             }
333             );
334              
335             # TODO: Have a look at sanitising the argument passed to the firewall.
336             my $routing_table = $self->_send_request(command => "<show><routing><route><virtual-router>$args{vrouter}</virtual-router></route></routing></show>");
337             return $routing_table->{entry};
338             }
339              
340              
341              
342             =head3 bgp_peers
343              
344             Retrieves information on the configured BGP peers for a particular virtual router. If no C<vrouter> argument is specified it retrieves the 'default' vrouter's BGP peers.
345              
346             my $default_vr_bgp_peers = $fw->bgp_peers();
347             my $corp_vr_bgp_peers = $fw->bgp_peers(vrouter => 'corp');
348              
349             =cut
350              
351             sub bgp_peers {
352             my $self = shift;
353             my %args = validate(@_,
354             {
355             vrouter => { default => 'default', type => SCALAR | UNDEF },
356             }
357             );
358              
359             # TODO: Have a look at sanitising the argument passed to the firewall.
360             my $bgp_peer_response = $self->_send_request(command =>
361             "<show><routing><protocol><bgp><peer><virtual-router>$args{vrouter}</virtual-router></peer></bgp></protocol></routing></show>"
362             );
363              
364             return if !defined $bgp_peer_response;
365              
366             return [] if !%{ $bgp_peer_response }; # No BGP peers configured.
367              
368             return $bgp_peer_response->{entry};
369             }
370              
371              
372              
373             =head3 bgp_rib
374              
375             Retrieves information the local routing information base (RIB) for a specific virtual router. If no C<vrouter> argument is specified, the 'default' vrouter's loc RIB is returned.
376              
377             my $default_vr_rib = $fw->bgp_rib();
378             my $corp_vr_rib = $fw->bgp_rib(vrouter => 'corp');
379              
380             If BGP is not configured, or there are no prefixes in the local RIB, an empty ARRAYREF is returned. Otherwise an ARRAYREF is returned containing the prefixes in the local RIB.
381              
382             =cut
383              
384             sub bgp_rib {
385             my $self = shift;
386             my %args = validate(@_,
387             {
388             vrouter => { default => 'default', type => SCALAR | UNDEF },
389             }
390             );
391              
392             # TODO: Have a look at sanitising the argument passed to the firewall.
393             my $bgp_rib = $self->_send_request(command =>
394             "<show><routing><protocol><bgp><loc-rib><virtual-router>$args{vrouter}</virtual-router></loc-rib></bgp></protocol></routing></show>"
395             );
396              
397             return if !defined $bgp_rib;
398              
399             # As we're only getting a single VR, there's only one array member, hence the [0].
400             my $rib_prefixes_ref = $bgp_rib->{entry}->[0]->{'loc-rib'};
401              
402             # Return and empty arrayref if there's nothing in the loc RIB.
403             return [] if !%{ $rib_prefixes_ref };
404              
405             return $rib_prefixes_ref->{member};
406             }
407              
408              
409              
410             =head3 ospf_neighbours
411              
412             Returns and ARRAYREF containing information on the current OSPF neighbours for a specific virtual router. If no C<vrouter> argument is specified, the 'default' vrouter's neighbours are returned.
413              
414             If OSPF is not configured, or there are no OSPF neighbours up, an empty ARRAYREF
415              
416             Neighbours are returned who have not completed a full OSPF handshake - for example they may be in EXSTART if there is an MTU mismatch on the interface.
417              
418             =cut
419              
420             sub ospf_neighbours {
421             my $self = shift;
422             my %args = validate(@_,
423             {
424             vrouter => { default => 'default', type => SCALAR | UNDEF },
425             }
426             );
427              
428             my $ospf_neighbours = $self->_send_request(command =>
429             "<show><routing><protocol><ospf><neighbor><virtual-router>$args{vrouter}</virtual-router></neighbor></ospf></protocol></routing></show>"
430             );
431              
432             return if !defined $ospf_neighbours;
433              
434             return [] if _is_null_response($ospf_neighbours->{entry});
435              
436             return $ospf_neighbours->{entry};
437             }
438              
439              
440             =head3 pim_neighbours
441              
442             Retrieves information on the PIM neighbours for a specific virtual router. If no C<vrouter> argument is specified, the neighbours for the 'default' vrouter are returned.
443              
444             my $pim_neighbours = $fw->pim_neighbours(vrouter => 'corp');
445              
446             If PIM is not configured, or there are currently no neighbours, an empty ARRAYREF is returned.
447              
448             =cut
449              
450             sub pim_neighbours {
451             my $self = shift;
452             my %args = validate(@_,
453             {
454             vrouter => { default => 'default', type => SCALAR | UNDEF },
455             }
456             );
457              
458             my $pim_neighbours = $self->_send_request(command =>
459             "<show><routing><multicast><pim><neighbor><virtual-router>$args{vrouter}</virtual-router></neighbor></pim></multicast></routing></show>"
460             );
461              
462             return if !defined $pim_neighbours;
463              
464             return [] if !%{ $pim_neighbours };
465              
466             return $pim_neighbours->{entry};
467             }
468              
469             =head3 bfd_peers
470              
471             Returns information on BFD peers.
472              
473             =cut
474              
475             sub bfd_peers {
476             my $self = shift;
477              
478             my $bfd_peers = $self->_send_request(command => '<show><routing><bfd><summary></summary></bfd></routing></show>');
479              
480             return if !defined $bfd_peers;
481              
482             return [] if !defined $bfd_peers->{entry};
483              
484             # The interfaces seem to have trailing whitespace, e.g.:
485             # $VAR1 = [ {
486             # 'status' => 'up',
487             # 'interface' => 'ethernet1/23 '
488             # }, ]
489             # We go through and remove it.
490             map { $_->{interface} =~ s{\s+$}{} } @{ $bfd_peers->{entry} };
491              
492             return $bfd_peers->{entry};
493             }
494              
495              
496              
497             =head2 MANAGEMENT
498              
499             These methods retrieve information on the management / operational status of the firewall.
500              
501             =head3 ntp
502              
503             Retrieves information on the current synchronisation and reachability of configured NTP peers.
504              
505             =cut
506              
507             sub ntp {
508             my $self = shift;
509             return $self->_send_request(command => "<show><ntp></ntp></show>");
510             }
511              
512              
513              
514             =head3 panorama_status
515              
516             Returns information on the current Panorama runtime status.
517              
518             =cut
519              
520             sub panorama_status {
521             my $self = shift;
522             my @ret;
523            
524             my $panorama_status_regex = qr{
525             Panorama\s+Server\s+(?<id>\d)
526             \s+ : \s+
527             (?<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})
528             \n
529             \s+ Connected \s+ : \s+ (?<connected>\w+)
530             \n
531             \s+ HA \s state \s+ : \s+ (?<ha_state>\w+)
532             }xms;
533              
534             my $panorama_status = $self->_send_request(command => '<show><panorama-status></panorama-status></show>');
535              
536             return if !defined $panorama_status;
537              
538             return [] if ref $panorama_status eq 'HASH' and !%{ $panorama_status };
539              
540             while ($panorama_status =~ m{$panorama_status_regex}g) {
541             my %pano_peer = %+;
542             push @ret, \%pano_peer;
543             }
544              
545             return \@ret;
546             }
547              
548              
549              
550             =head2 SECURITY
551              
552             These methods retrieve information on the security functions of the firewall.
553              
554             =head3 ip_user_mapping
555              
556             Returns the ip to user mapping table.
557              
558             =cut
559              
560             sub ip_user_mapping {
561             my $self = shift;
562              
563             my $ip_user_mappings = $self->_send_request(command => '<show><user><ip-user-mapping><all></all></ip-user-mapping></user></show>');
564              
565             return if !defined $ip_user_mappings;
566              
567              
568             return [] if !%{ $ip_user_mappings };
569              
570             # Split the user and domain into their own keys
571             IP_USER_MAP:
572             for my $user_map (@{ $ip_user_mappings->{entry} }) {
573             if (lc $user_map->{user} eq 'unknown') {
574             $user_map->{domain} = 'unknown';
575             next IP_USER_MAP;
576             }
577              
578              
579             # Split on the backslash
580             my @domain_and_user = split(m{\\}, $user_map->{user});
581             carp "User to IP mapping contains no deliniaton ('\\') between domain and user: $user_map->{user}" if @domain_and_user != 2;
582            
583             $user_map->{domain} = $domain_and_user[0];
584             $user_map->{user} = $domain_and_user[1];
585             }
586              
587              
588             return $ip_user_mappings->{entry};
589             }
590              
591              
592              
593             =head3 userid_server_monitor
594              
595             Returns the state of the servers used to monitor User-ID IP-to-user mappings.
596              
597             =cut
598              
599             sub userid_server_monitor {
600             my $self = shift;
601             my @ret;
602              
603             my $server_monitor = $self->_send_request(command => '<show><user><server-monitor><statistics></statistics></server-monitor></user></show>');
604              
605             return if !defined $server_monitor;
606              
607             return {} if !$server_monitor;
608              
609             # Clean up the output, turning it into an ARRARREF rather than a HASHREF keyed on the server name
610             for my $server (keys %{ $server_monitor->{entry} }) {
611             $server_monitor->{entry}->{ $server }->{name} = $server;
612             push @ret, $server_monitor->{entry}->{ $server };
613             }
614              
615             return \@ret;
616             }
617              
618              
619              
620             =head3 ike_peers
621              
622             Returns information on active IKE (Phase 1) VPN peers.
623              
624             =cut
625              
626             sub ike_peers {
627             my $self = shift;
628              
629             my $ike_peers = $self->_send_request(command => '<show><vpn><ike-sa></ike-sa></vpn></show>');
630              
631             return if !defined $ike_peers;
632              
633             return [] if !%{ $ike_peers };
634              
635             return $ike_peers->{entry};
636             }
637              
638              
639              
640             =head3 ipsec_peers
641              
642             Returns information on the active IPSEC (Phase 2) VPN peers.
643              
644             =cut
645              
646             sub ipsec_peers {
647             my $self = shift;
648              
649             my $ipsec_peers = $self->_send_request(command => '<show><vpn><ipsec-sa></ipsec-sa></vpn></show>');
650              
651             return if !defined $ipsec_peers;
652              
653             return [] if !%{ $ipsec_peers->{entries} };
654              
655             return $ipsec_peers->{entries}->{entry};
656             }
657              
658              
659              
660             =head3 vpn_tunnels
661              
662             Returns dataplane IPSEC VPN tunnel information.
663              
664             =cut
665              
666             sub vpn_tunnels {
667             my $self = shift;
668              
669             my $vpn_tunnels = $self->_send_request(command => '<show><vpn><flow></flow></vpn></show>');
670              
671             return if !defined $vpn_tunnels;
672              
673             return [] if !%{ $vpn_tunnels->{IPSec} };
674              
675             return $vpn_tunnels->{IPSec}->{entry};
676              
677             }
678              
679              
680              
681              
682              
683              
684              
685             ####################
686             # Utility Functions
687             #
688             ####################
689              
690              
691             sub _send_request {
692             my $self = shift;
693             my %args = validate(@_,
694             {
695             command => 1,
696             }
697             );
698              
699             # Is the API key defined? If not, request one.
700             if (!defined $self->_api_key) {
701             return if !$self->authenticate();
702             }
703              
704             #Set up the query string and the HTTP request
705             $self->uri->query( "type=op&cmd=$args{command}&key=".$self->_api_key );
706             $self->http_request->uri( $self->uri->as_string );
707              
708             # Clear the query
709             $self->uri->query(undef);
710              
711             # Send and get the HTTP response and check it for errors
712             my $http_response = $self->_send_http_request();
713             return if !$self->_check_http_response($http_response);
714              
715             # Get the PA response (XML to a Perl Structure) from the body and check for errors
716             my $pa_response = $self->_get_pa_response($http_response);
717             return if !$self->_check_pa_response($pa_response);
718              
719             # Return the structure
720             return $pa_response->{result};
721             }
722              
723              
724              
725             sub _send_http_request {
726             my $self = shift;
727            
728             return $self->user_agent->request( $self->http_request );
729              
730             }
731              
732             sub _check_http_response {
733             my $self = shift;
734             my $http_response = shift;
735              
736             # Check the HTTP response codes
737             if ($http_response->is_error) {
738             carp "HTTP Error (".$http_response->code.")";
739             return;
740             }
741              
742             return 1;
743             }
744              
745             sub _get_pa_response {
746             my $self = shift;
747             my $http_response = shift;
748             my $xml_parser = XML::Twig->new();
749              
750             my $pa_response = $xml_parser->safe_parse( $http_response->decoded_content )->simplify( forcearray => ['entry']);
751              
752             return $pa_response;
753             }
754              
755              
756             sub _check_pa_response {
757             my $self = shift;
758             my $pa_response = shift;
759              
760             if ($pa_response->{status} eq 'error') {
761             carp "API Error: ".$self->_api_error_to_string($pa_response->{code});
762             return;
763             }
764              
765             return $pa_response;
766             }
767              
768              
769              
770             sub _is_null_response {
771             my $response = shift;
772              
773             if (!$response
774             || (ref $response eq 'ARRAY' and !@{ $response })
775             || (ref $response eq 'HASH' and !%{ $response })) {
776             return 1;
777             }
778              
779             return 0;
780             }
781              
782              
783             sub _api_error_to_string {
784             my $self = shift;
785             my $code = shift;
786              
787             return {
788             400 => 'Bad request (400)',
789             403 => 'Forbidden (403)',
790             1 => 'Unknown command (1)',
791             2 => 'Internal error (2)',
792             3 => 'Internal error (3)',
793             4 => 'Internal error (4)',
794             5 => 'Internal error (5)',
795             6 => 'Bad Xpath (6)',
796             7 => 'Object not present (7)',
797             8 => 'Object not unique (8)',
798             10 => 'Reference count not zero (10)',
799             11 => 'Internal error (11)',
800             12 => 'Invalid object (12)',
801             14 => 'Operation not possible (14)',
802             15 => 'Operation denied (15)',
803             16 => 'Unauthorized (16)',
804             17 => 'Invalid command (17)',
805             18 => 'Malformed (18)',
806             19 => 'Success (19)',
807             20 => 'Success (20)',
808             21 => 'Internal error (21)',
809             22 => 'Session timed out (22)',
810             }->{$code} // "Unknown Error Code";
811             }
812              
813              
814             sub _debug_print {
815             my $self = shift;
816             my $debug_msg = shift;
817             my $debug_structure = shift;
818              
819              
820             print STDERR $debug_msg."\n" if $self->debug == 1;
821             print STDERR (Dumper $debug_structure) if $debug_structure;
822              
823             return;
824             }
825              
826              
827             =head1 AUTHOR
828              
829             Greg Foletta, C<< <greg at foletta.org> >>
830              
831             =head1 BUGS
832              
833             Please report any bugs or feature requests to C<bug-device-paloalto-firewall at rt.cpan.org>, or through
834             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Device-PaloAlto-Firewall>. I will be notified, and then you'll
835             automatically be notified of progress on your bug as I make changes.
836              
837              
838              
839              
840             =head1 SUPPORT
841              
842             You can find documentation for this module with the perldoc command.
843              
844             perldoc Device::PaloAlto::Firewall
845              
846              
847             You can also look for information at:
848              
849             =over 4
850              
851             =item * RT: CPAN's request tracker (report bugs here)
852              
853             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Device-PaloAlto-Firewall>
854              
855             =item * AnnoCPAN: Annotated CPAN documentation
856              
857             L<http://annocpan.org/dist/Device-PaloAlto-Firewall>
858              
859             =item * CPAN Ratings
860              
861             L<http://cpanratings.perl.org/d/Device-PaloAlto-Firewall>
862              
863             =item * Search CPAN
864              
865             L<http://search.cpan.org/dist/Device-PaloAlto-Firewall/>
866              
867             =back
868              
869              
870             =head1 ACKNOWLEDGEMENTS
871              
872              
873             =head1 LICENSE AND COPYRIGHT
874              
875             Copyright 2017 Greg Foletta.
876              
877             This program is free software; you can redistribute it and/or modify it
878             under the terms of the the Artistic License (2.0). You may obtain a
879             copy of the full license at:
880              
881             L<http://www.perlfoundation.org/artistic_license_2_0>
882              
883             Any use, modification, and distribution of the Standard or Modified
884             Versions is governed by this Artistic License. By using, modifying or
885             distributing the Package, you accept this license. Do not use, modify,
886             or distribute the Package, if you do not accept this license.
887              
888             If your Modified Version has been derived from a Modified Version made
889             by someone other than you, you are nevertheless required to ensure that
890             your Modified Version complies with the requirements of this license.
891              
892             This license does not grant you the right to use any trademark, service
893             mark, tradename, or logo of the Copyright Holder.
894              
895             This license includes the non-exclusive, worldwide, free-of-charge
896             patent license to make, have made, use, offer to sell, sell, import and
897             otherwise transfer the Package with respect to any patent claims
898             licensable by the Copyright Holder that are necessarily infringed by the
899             Package. If you institute patent litigation (including a cross-claim or
900             counterclaim) against any party alleging that the Package constitutes
901             direct or contributory patent infringement, then this Artistic License
902             to you shall terminate on the date that such litigation is filed.
903              
904             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
905             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
906             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
907             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
908             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
909             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
910             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
911             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
912              
913              
914             =cut
915              
916             1; # End of Device::PaloAlto::Firewall