File Coverage

lib/Net/ISC/DHCPd/OMAPI.pm
Criterion Covered Total %
statement 27 111 24.3
branch 1 42 2.3
condition n/a
subroutine 9 14 64.2
pod 3 3 100.0
total 40 170 23.5


line stmt bran cond sub pod time code
1             package Net::ISC::DHCPd::OMAPI;
2              
3             =head1 NAME
4              
5             Net::ISC::DHCPd::OMAPI - Talk to a dhcp server
6              
7             =head1 SYNOPSIS
8              
9             my $omapi = Net::ISC::DHCPd::OMAPI->new(
10             key => "dhcpd secret",
11             );
12              
13             # connect is lazy
14             $omapi->connect
15              
16             my $lease = $omapi->new_object(lease => (
17             ip_address => "10.19.83.200",
18             ));
19              
20             if($lease->read) {
21             printf("Got hardware_address=%s from ip_address=%s\n",
22             $lease->hardware_address,
23             $lease->ip_address,
24             );
25             }
26              
27             =head1 DESCRIPTION
28              
29             This module provides an API to query and possible change the ISC DHCPd
30             server. The module use OMAPI (Object Management API) which does not
31             require the server to be restarted for changes to apply. It does
32             unfortunately support the protocol natively, but instead fork
33             C<omshell(1)> which this module read and write commands to.
34              
35             OMAPI is simply a communications mechanism that allows you to manipulate
36             objects, which is stored in the dhcpd.leases file.
37              
38             See subclasses for more information about the different objects you can
39             manipulate:
40             L<Net::ISC::DHCPd::OMAPI::Failover>,
41             L<Net::ISC::DHCPd::OMAPI::Group>,
42             L<Net::ISC::DHCPd::OMAPI::Host>,
43             and L<Net::ISC::DHCPd::OMAPI::Lease>.
44              
45             =head1 ENVIRONMENT VARIABLES
46              
47             =over 4
48              
49             =item * DHCP_OMAPI_DEBUG=1
50              
51             This variable will enable debug output.
52              
53             =back
54              
55             =cut
56              
57 1     1   9315 use Moose;
  1         2  
  1         8  
58 1     1   5255 use IO::Pty;
  1         7935  
  1         44  
59 1     1   474 use Time::HiRes qw/usleep/;
  1         1170  
  1         3  
60 1     1   445 use Net::ISC::DHCPd::OMAPI::Control;
  1         2  
  1         39  
61 1     1   513 use Net::ISC::DHCPd::OMAPI::Failover;
  1         2  
  1         39  
62 1     1   507 use Net::ISC::DHCPd::OMAPI::Group;
  1         3  
  1         38  
63 1     1   510 use Net::ISC::DHCPd::OMAPI::Host;
  1         3  
  1         45  
64 1     1   489 use Net::ISC::DHCPd::OMAPI::Lease;
  1         4  
  1         54  
65 1 50   1   7 use constant DEBUG => $ENV{DHCP_OMAPI_DEBUG} ? 1 : 0;
  1         2  
  1         964  
66              
67             our $OMSHELL = 'omshell';
68              
69             =head1 ATTRIBUTES
70              
71             =head2 server
72              
73             This attribute is read-only and holds a string describing the
74             remote dhcpd server address. Default value is "127.0.0.1".
75              
76             =cut
77              
78             has server => (
79             is => 'ro',
80             isa => 'Str',
81             default => '127.0.0.1',
82             );
83              
84             =head2 port
85              
86             This attribute is read-only and holds an integer representing
87             the remote dhcpd server port. Default value is "7911".
88              
89             =cut
90              
91             has port => (
92             is => 'ro',
93             isa => 'Int',
94             default => 7911,
95             );
96              
97             =head2 key
98              
99             This attribute is read-only and holds a string representing the
100             server secret key. It is in the format C<$name $secret> and the
101             default value is an empty string. An empty string is used for
102             servers without a secret to log in.
103              
104             =cut
105              
106             has key => (
107             is => 'ro',
108             isa => 'Str',
109             default => '',
110             );
111              
112             =head2 errstr
113              
114             Holds the last know error as a plain string.
115              
116             =cut
117              
118             has errstr => (
119             is => 'rw',
120             isa => 'Str',
121             default => '',
122             );
123              
124             # meant for internal usage
125             has _fh => (
126             is => 'ro',
127             lazy => 1,
128             builder => '_build__fh',
129             clearer => '_clear__fh',
130             );
131              
132             has _pid => (
133             is => 'rw',
134             isa => 'Int',
135             );
136              
137             # fork omshell and return an IO::Pty object
138             sub _build__fh {
139 0     0     my $self = shift;
140 0           my $pty = IO::Pty->new;
141 0           my($pid, $slave);
142              
143 0 0         pipe my $READ, my $WRITE or confess $!;
144 0           select +(select($WRITE), $|++)[0]; # autoflush
145              
146 0           $pid = fork;
147              
148 0 0         if(!defined $pid) { # failed
    0          
149 0           $self->errstr($@ = $!);
150 0           return;
151             }
152             elsif($pid) { # parent
153 0           close $WRITE;
154 0           $pty->close_slave;
155 0           $pty->set_raw;
156 0           $self->_pid($pid);
157              
158 0 0         if(my $error = sysread $READ, my $errno, 255) {
159 0           $! = $errno + 0;
160 0           confess "Could not exec $OMSHELL: $!";
161             }
162 0 0         if(!defined $pty->sysread(my $buffer, 2048)) {
163 0           return;
164             }
165              
166 0           return $pty;
167             }
168             else { # child
169 0           close $READ;
170 0           $pty->make_slave_controlling_terminal;
171 0           $slave = $pty->slave;
172 0           $slave->set_raw;
173              
174 0 0         open STDIN, '<&'. $slave->fileno or confess "Reopen STDIN: $!";
175 0 0         open STDOUT, '>&'. $slave->fileno or confess "Reopen STDOUT: $!";
176 0 0         open STDERR, '>&'. $slave->fileno or confess "Reopen STDERR: $!";
177              
178 0           { exec $OMSHELL } # block prevent warning
  0            
179 0           print $WRITE int $!;
180 0           confess "Could not exec $OMSHELL: $!";
181             }
182             }
183              
184             # $self->_cmd($cmd);
185             sub _cmd {
186 0     0     my $self = shift;
187 0           my $cmd = shift;
188 0           my $pty = $self->_fh;
189 0           my $out = q();
190 0           my $end_time;
191              
192 0           print STDERR "\$ $cmd\n" if DEBUG;
193              
194 0 0         unless(defined $pty->syswrite("$cmd\n")) {
195 0           $self->errstr($!);
196 0           return;
197             }
198              
199 0           $end_time = time + 10;
200              
201             BUFFER:
202 0           while(time < $end_time) {
203 0 0         if(defined $pty->sysread(my $tmp, 1024)) {
204 0           $out .= $tmp;
205 0 0         $out =~ s/>\s$// and last BUFFER;
206             }
207             else {
208 0           $self->errstr($!);
209 0           return;
210             }
211             }
212              
213 0           $out =~ s/^>\s//;
214              
215 0           print STDERR $out if DEBUG;
216              
217 0           return $out;
218             }
219              
220             =head1 METHODS
221              
222             =head2 connect
223              
224             $bool = $self->connect;
225              
226             Will open a connection to the dhcp server. Check L</errstr> on failure.
227             A connection means starting the program C<omshell(1)> and trying to
228             log in, if the dhcpd L</key> is set.
229              
230             =cut
231              
232             sub connect {
233 0     0 1   my $self = shift;
234 0           my @commands = qw/port server/;
235 0           my $buffer;
236              
237 0 0         push @commands, 'key' if($self->key);
238              
239 0           $self->errstr('');
240              
241 0           for my $attr (@commands) {
242 0           $buffer = $self->_cmd(sprintf "%s %s", $attr, $self->$attr);
243 0 0         last unless(defined $buffer);
244             }
245              
246 0 0         if($self->errstr) {
247 0           return;
248             }
249 0 0         unless($buffer = $self->_cmd('connect')) {
250 0           return;
251             }
252 0 0         unless($buffer =~ /obj:\s+/) {
253 0           $self->errstr($buffer);
254 0           return;
255             }
256              
257 0           return 1;
258             }
259              
260             =head2 disconnect
261              
262             $bool = $self->disconnect;
263              
264             Will disconnect from the server. This means killing the C<omshell(1)>
265             program, which then actually will make sure the connection is shut
266             down.
267              
268             =cut
269              
270             sub disconnect {
271 0     0 1   my $self = shift;
272 0           my $retries = 10;
273              
274 0           while($retries--) {
275 0           kill 15, $self->_pid;
276 0           usleep 2e3;
277 0 0         if(kill 0, $self->_pid) {
278 0           $retries = 1; # make sure it's true
279 0           last;
280             }
281             }
282              
283 0 0         unless($retries) {
284 0           return;
285             }
286              
287 0           $self->_clear__fh;
288              
289 0           return 1;
290             }
291              
292             =head2 new_object
293              
294             $object = $self->new_object($type => %constructor_args);
295              
296             This method will create a new OMAPI object, which can be used to query
297             and/or manipulate the running dhcpd server.
298              
299             C<$type> can be "group", "host", or "lease". Will return a new config object.
300              
301             Example, with C<$type='host'>:
302              
303             Net::ISC::DHCPd::Config::Host->new(%constructor_args);
304              
305             =cut
306              
307             sub new_object {
308 0     0 1   my $self = shift;
309 0 0         my $type = shift or return;
310 0           my %args = @_;
311 0           my $class = 'Net::ISC::DHCPd::OMAPI::' .ucfirst(lc $type);
312              
313 0 0         unless($type =~ /^(?:control|failover|group|host|lease)$/i) {
314 0           return;
315             }
316              
317 0           return $class->new(parent => $self, %args);
318             }
319              
320             =head1 COPYRIGHT & LICENSE
321              
322             =head1 AUTHOR
323              
324             See L<Net::ISC::DHCPd>.
325              
326             =cut
327             __PACKAGE__->meta->make_immutable;
328             1;