Designate forbidden address ranges, #25530
[freeside.git] / FS / FS / addr_range.pm
1 package FS::addr_range;
2
3 use strict;
4 use base qw( FS::Record );
5 use vars qw( %status_desc
6              %status_allow_auto
7              %status_allow_use
8            );
9 use FS::Record qw( qsearch qsearchs );
10 use NetAddr::IP;
11
12 # metadata about status strings:
13 # how to describe them
14 %status_desc = (
15   ''            => '',
16   'unavailable' => 'unavailable',
17 );
18
19 # whether addresses in this range are available for use
20 %status_allow_use = (
21   ''            => 1,
22   'unavailable' => 0,
23 );
24
25 =head1 NAME
26
27 FS::addr_range - Object methods for addr_range records
28
29 =head1 SYNOPSIS
30
31   use FS::addr_range;
32
33   $record = new FS::addr_range \%hash;
34   $record = new FS::addr_range { 'column' => 'value' };
35
36   $error = $record->insert;
37
38   $error = $new_record->replace($old_record);
39
40   $error = $record->delete;
41
42   $error = $record->check;
43
44 =head1 DESCRIPTION
45
46 An FS::addr_range object represents a contiguous range of IP 
47 addresses assigned to a certain purpose.  Unlike L<FS::addr_block>,
48 this isn't a routing block; the range doesn't have to be aligned on 
49 a subnet boundary, and doesn't have a gateway or broadcast address.
50 It's just a range.
51
52 =over 4
53
54 =item rangenum - primary key
55
56 =item start - starting address of the range, as a dotted quad
57
58 =item length - number of addresses in the range, including start
59
60 =item status - what to do with the addresses in this range; currently can 
61 only be "unavailable", which makes the addresses unavailable for assignment 
62 to any kind of service.
63
64 =back
65
66 =head1 METHODS
67
68 =over 4
69
70 =item new HASHREF
71
72 Creates a new range.  To add the example to the database, see L<"insert">.
73
74 Note that this stores the hash reference, not a distinct copy of the hash it
75 points to.  You can ask the object for a copy with the I<hash> method.
76
77 =cut
78
79 sub table { 'addr_range'; }
80
81 =item insert
82
83 Adds this record to the database.  If there is an error, returns the error,
84 otherwise returns false.
85
86 =cut
87
88 # the insert method can be inherited from FS::Record
89
90 =item delete
91
92 Delete this record from the database.
93
94 =cut
95
96 # the delete method can be inherited from FS::Record
97
98 =item replace OLD_RECORD
99
100 Replaces the OLD_RECORD with this one in the database.  If there is an error,
101 returns the error, otherwise returns false.
102
103 =cut
104
105 # the replace method can be inherited from FS::Record
106
107 =item check
108
109 Checks all fields to make sure this is a valid example.  If there is
110 an error, returns the error, otherwise returns false.  Called by the insert
111 and replace methods.
112
113 =cut
114
115 # the check method should currently be supplied - FS::Record contains some
116 # data checking routines
117
118 sub check {
119   my $self = shift;
120
121   my $error = 
122     $self->ut_numbern('rangenum')
123     || $self->ut_ip('start')
124     || $self->ut_number('length')
125     || $self->ut_textn('status')
126   ;
127   return $error if $error;
128
129   $self->SUPER::check;
130 }
131
132 =item end [ IPADDR ]
133
134 Get/set the end IP address in the range.  This isn't actually part of the
135 record but it's convenient.
136
137 =cut
138
139 sub end {
140   my $self = shift;
141   # if there's no start address, just return nothing
142   my $start = NetAddr::IP->new($self->start, 0) or return '';
143
144   my $new = shift;
145   if ( $new ) {
146     my $end = NetAddr::IP->new($new, 0)
147       or die "bad end address $new";
148     if ( $end < $start ) {
149       $self->set('start', $end);
150       ($end, $start) = ($start, $end);
151     }
152     $self->set('length', $end - $start + 1);
153     return $end->addr;
154   }
155   my $end = $start + $self->get('length') - 1;
156   $end->addr;
157 }
158
159 =item contains IPADDR
160
161 Checks whether IPADDR (a dotted-quad IPv4 address) is within the range.
162
163 =cut
164
165 sub contains {
166   my $self = shift;
167   my $addr = shift;
168   $addr = NetAddr::IP->new($addr, 0)
169     unless ref($addr) and UNIVERSAL::isa($addr, 'NetAddr::IP');
170   return 0 unless $addr;
171
172   my $start = NetAddr::IP->new($self->start, 0);
173
174   return ($addr >= $start and $addr - $start < $self->length) ? 1 : 0;
175
176
177 =item as_string
178
179 Returns a readable string showing the address range.
180
181 =cut
182
183 sub as_string {
184   my $self = shift;
185   my $start = NetAddr::IP->new($self->start, 0);
186   my $end   = $start + $self->length;
187
188   if ( $self->length == 1 ) {
189     # then just the address
190     return $self->start;
191   } else { # we have to get tricksy
192     my @end_octets = split('\.', $end->addr);
193     $start = ($start->numeric)[0] + 0;
194     $end   = ($end->numeric)[0] + 0;
195     # which octets are different between start and end?
196     my $delta = $end ^ $start;
197     foreach (0xffffff, 0xffff, 0xff) {
198       if ( $delta <= $_ ) {
199       # then they are identical in the first 8/16/24 bits
200         shift @end_octets;
201       }
202     }
203     return $self->start . '-' . join('.', @end_octets);
204   }
205 }
206
207 =item desc
208
209 Returns a semi-friendly description of the block status.
210
211 =item allow_use
212
213 Returns true if addresses in this range can be used by services, etc.
214
215 =cut
216
217 sub desc {
218   my $self = shift;
219   $status_desc{ $self->status };
220 }
221
222 sub allow_auto {
223   my $self = shift;
224   $status_allow_auto{ $self->status };
225 }
226
227 sub allow_use {
228   my $self = shift;
229   $status_allow_use{ $self->status };
230 }
231
232 =back
233
234 =head1 CLASS METHODS
235
236 =sub any_contains IPADDR
237
238 Returns all address ranges that contain IPADDR.
239
240 =cut
241
242 sub any_contains {
243   my $self = shift;
244   my $addr = shift;
245   return grep { $_->contains($addr) } qsearch('addr_range', {});
246 }
247
248 =head1 DEVELOPER NOTE
249
250 L<NetAddr::IP> objects have netmasks.  When using them to represent 
251 range endpoints, be sure to set the netmask to I<zero> so that math on 
252 the address doesn't stop at the subnet boundary.  (The default is /32, 
253 which doesn't work very well.  Address ranges ignore subnet boundaries.
254
255 =head1 BUGS
256
257 =head1 SEE ALSO
258
259 L<FS::svc_IP_Mixin>, L<FS::Record>, schema.html from the base documentation.
260
261 =cut
262
263 1;
264