File Coverage

blib/lib/StorageDisplay.pm
Criterion Covered Total %
statement 297 339 87.6
branch 48 70 68.5
condition 29 41 70.7
subroutine 50 53 94.3
pod 0 29 0.0
total 424 532 79.7


line stmt bran cond sub pod time code
1             #
2             # This file is part of StorageDisplay
3             #
4             # This software is copyright (c) 2014-2023 by Vincent Danjean.
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9 9     9   67 use strict;
  9         23  
  9         398  
10 9     9   56 use warnings;
  9         18  
  9         648  
11 9     9   128 use 5.14.0;
  9         37  
12              
13             package StorageDisplay;
14             # ABSTRACT: Collect and display storages on linux machines
15              
16             our $VERSION = '2.06'; # VERSION
17              
18             ## Main object
19              
20 9     9   5618 use Moose;
  9         5190958  
  9         81  
21 9     9   84373 use namespace::sweep;
  9         93364  
  9         109  
22 9     9   751 use Carp;
  9         22  
  9         604  
23 9     9   4672 use StorageDisplay::Block;
  9         354  
  9         565  
24 9     9   5867 use StorageDisplay::Data::Root;
  9         227  
  9         536  
25 9     9   6938 use StorageDisplay::Data::Partition;
  9         763  
  9         678  
26 9     9   6632 use StorageDisplay::Data::LVM;
  9         940  
  9         1413  
27 9     9   9557 use StorageDisplay::Data::RAID;
  9         1749  
  9         1035  
28 9     9   6515 use StorageDisplay::Data::LUKS;
  9         513  
  9         675  
29 9     9   6582 use StorageDisplay::Data::FS;
  9         755  
  9         825  
30 9     9   7826 use StorageDisplay::Data::Libvirt;
  9         518  
  9         653  
31 9     9   6510 use StorageDisplay::Data::Loop;
  9         190  
  9         46153  
32              
33             has 'blocks' => (
34             is => 'ro',
35             isa => 'HashRef[StorageDisplay::Block]',
36             traits => [ 'Hash' ],
37             default => sub { return {}; },
38             lazy => 1,
39             handles => {
40             'addBlock' => 'set',
41             'has_block' => 'exists',
42             '_block' => 'get',
43             'allBlocks' => 'values'
44             },
45             );
46              
47             has 'blocksRoot' => (
48             is => 'ro',
49             isa => 'StorageDisplay::BlockTreeElement',
50             lazy => 1,
51             builder => '_loadAllBlocks',
52             );
53              
54             has 'infos' => (
55             is => 'ro',
56             isa => 'HashRef',
57             required => 1,
58             traits => [ 'Hash' ],
59             # handles => {
60             # 'get_info' => 'get',
61             # }
62             );
63              
64             sub get_info {
65 1072     1072 0 2065 my $self = shift ;
66 1072         3303 my @keys=@_;
67              
68 1072         39758 my $infos=$self->infos;
69              
70 1072         3745 while (defined(my $k = shift @keys)) {
71 2347 100       7030 return if not defined($infos->{$k});
72 2306         6769 $infos = $infos->{$k};
73             }
74 1031         4106 return $infos;
75             }
76              
77             #has 'connect' => (
78             # is => 'ro',
79             # isa => 'StorageDisplay::Connect',
80             # required => 0,
81             # );
82              
83             sub _allocateBlock {
84 1328     1328   2844 my $self=shift;
85 1328         2399 my $name=shift;
86 1328         2244 my $alloc=shift;
87              
88 1328 100       63333 if (! $self->has_block($name)) {
89 721         1871 my $block=$alloc->();
90 721         1102055 foreach my $n ($block->names_str()) {
91 1677 100       76497 if ($self->has_block($n)) {
92 6         259 print STDERR "W: duplicate block name '$n' for ".$block->name.
93             " and ".$self->_block($n)->name."\n";
94             } else {
95             #print STDERR "I: in $self Registering block name '$n' for ".$block->name."\n";
96             }
97 1677         70020 $self->addBlock($n, $block);
98             }
99             }
100 1328         58228 return $self->_block($name);
101             }
102              
103             sub systemBlock {
104 336     336 0 590 my $self=shift;
105 336         680 my $name=shift;
106              
107             return $self->_allocateBlock(
108             $name, sub {
109 233     233   1335 return StorageDisplay::Block::System->new(
110             $name,
111             $self);
112 336         2291 });
113             }
114              
115             sub block {
116 992     992 0 2113 my $self=shift;
117 992         1718 my $name=shift;
118              
119 992 100       4042 if ($name =~m,^/dev/(.*)$,) {
120 208         633 $name=$1;
121             }
122             return $self->_allocateBlock(
123             $name, sub {
124 488     488   2817 return StorageDisplay::Block::NoSystem->new(
125             'name' => $name,
126             );
127 992         7282 });
128             }
129              
130             sub blockBySerial {
131 4     4 0 7 my $self=shift;
132 4         9 my $serial=shift;
133              
134 4         143 foreach my $block ($self->allBlocks()) {
135             #print STDERR " Testing ", ($block->name), "\n";
136 259 100 100     7101 if (($block->blk_info('SERIAL')//'') eq $serial) {
137 2         13 return $block;
138             }
139 257 50 100     6825 if (($block->udev_info('ID_SCSI_SERIAL')//'') eq $serial) {
140 0         0 return $block;
141             }
142             # WWN is not always unique :-(
143             #$serial =~ s/^0x//;
144             #if (($block->blk_info('WWN')//'') eq $serial) {
145             # return $block;
146             #}
147             #if (($block->blk_info('WWN')//'') eq '0x'.$serial) {
148             # return $block;
149             #}
150             }
151             #print STDERR "$serial not found in $self\n";
152 2         26 return;
153             }
154              
155             sub _loadAllBlocks {
156 7     7   21 my $self=shift;
157              
158 7         42 my $blocks=$self->get_info('lsblk-hierarchy');
159              
160 7         16 my $handle_bloc;
161             $handle_bloc = sub {
162 336     336   594 my $jcur = shift;
163 336         568 my $bparent = shift;
164 336   100     576 my @children = (@{$jcur->{'children'}//[]});
  336         1726  
165             #print STDERR Dumper($jcur);
166 336         1521 my $bcur = $self->systemBlock($jcur->{'kname'});
167 336         2925 $bparent->addChild($bcur);
168 336         748 foreach my $jchild (@children) {
169 262         883 my $bchild = $handle_bloc->($jchild, $bcur);
170             }
171 336         1005 return $bcur;
172 7         48 };
173              
174 7         138 my $root=StorageDisplay::BlockTreeElement->new('name' => 'Root');
175              
176 7         4884 foreach my $b (values %$blocks) {
177 74         251 $handle_bloc->($b, $root);
178             }
179 7         183 return $root;
180             }
181              
182             sub dumpBlocks {
183 0     0 0 0 my $self = shift;
184              
185 0         0 foreach my $b ($self->allBlocks) {
186 0         0 print $b->name, "\n";
187             }
188             }
189              
190             sub _log {
191 481     481   923 my $self = shift;
192 481         841 my $opts = shift;
193 481         889 my $info = shift;
194              
195 481 100       2330 if (ref($info) =~ /^HASH/) {
196 391         865 $opts = { %{$opts}, %{$info} };
  391         1361  
  391         1611  
197 391         1079 $info = shift;
198             }
199              
200 481         40482 print STDERR $opts->{type}, ': ', (' 'x$opts->{level}), $info, "\n";
201 481         3358 foreach my $line (@_) {
202 0         0 print STDERR ' ', (' 'x$opts->{level}), $line, "\n";
203             }
204             }
205              
206              
207             sub log {
208 481     481 0 962 my $self = shift;
209              
210 481         3713 return $self->_log(
211             {
212             'level' => 0,
213             'type' => 'I',
214             'verbose' => 1,
215             }, @_);
216             }
217              
218             sub warn {
219 0     0 0 0 my $self = shift;
220              
221 0         0 return $self->_log(
222             {
223             'level' => 0,
224             'type' => 'W',
225             'verbose' => 1,
226             }, @_);
227             }
228              
229             sub error {
230 0     0 0 0 my $self = shift;
231              
232 0         0 return $self->_log(
233             {
234             'level' => 0,
235             'type' => 'E',
236             'verbose' => 1,
237             }, @_);
238             }
239              
240             ###################
241             has '_providedBlocks' => (
242             is => 'ro',
243             isa => 'HashRef[StorageDisplay::Data::Elem]',
244             traits => [ 'Hash' ],
245             default => sub { return {}; },
246             lazy => 1,
247             handles => {
248             '_addProvidedBlock' => 'set',
249             '_provideBlock' => 'exists',
250             }
251             );
252              
253             has 'elemsRoot' => (
254             is => 'ro',
255             isa => 'StorageDisplay::Data::Root',
256             default => sub {
257             my $self = shift;
258             return StorageDisplay::Data::Root->new(
259             $self->get_info('hostname'));
260             },
261             lazy => 1,
262             );
263              
264             sub _registerElement {
265 62     62   199 my $self = shift;
266 62         174 my $elem = shift;
267             my @providedBlockNames = map {
268 62         3750 StorageDisplay::Block::asname($_)
  3         16  
269             } $elem->allProvidedBlocks;
270              
271 62         185 foreach my $bn (@providedBlockNames) {
272 3 50       13 if ($self->provide($bn)) {
273 0         0 carp "Duplicate provider for $bn";
274 0         0 return 0;
275             }
276             }
277 62         151 foreach my $bn (@providedBlockNames) {
278 3         141 $self->_addProvidedBlock($bn, $elem);
279             }
280             #use Data::Dumper;
281             #print STDERR Dumper($elem);
282             #print STDERR $elem->isa("StorageDisplay::Data::Elem"), " DONE\n";
283 62         2397 $self->elemsRoot->addChild($elem);
284 62         373 return 1;
285             }
286              
287             sub provide {
288 3     3 0 9 my $self = shift;
289 3         35 my $block = shift;
290 3         13 my $blockname = StorageDisplay::Block::asname($block);
291              
292 3         147 return $self->_provideBlock($blockname);
293             }
294              
295             sub createElems {
296 7     7 0 22 my $self = shift;
297 7         269 $self->blocksRoot();
298 7         223 my $root=$self->elemsRoot;
299 7         67 $self->removeVMsPartitions;
300 7         52 $self->createPartitionTables($root);
301 7         52 $self->createLVMs($root);
302 7         56 $self->createLUKSs($root);
303 7         46 $self->createMDs($root);
304 7         50 $self->createLSIMegaclis($root);
305 7         48 $self->createLSISASIrcus($root);
306 7         61 $self->createFSs($root);
307 7         44 $self->createVMs($root);
308 7         52 $self->createLoops($root);
309             # Must be last, to avoid to create already existing disks
310 7         71 $self->createEmptyDisks($root);
311 7         42 $self->computeUsedBlocks;
312             }
313              
314             sub removeVMsPartitions {
315 7     7 0 20 my $self = shift;
316 7   50     33 my $partitions = $self->get_info('partitions')//{};
317 7   100     26 my $vms = $self->get_info('libvirt')//{};
318 7         20 my $vmblocks={};
319 7         40 $self->log("Removing partitions of virtual machines disks");
320 7         37 foreach my $vm (values %$vms) {
321 35   100     49 foreach my $bname (keys %{$vm->{blocks}//{}}) {
  35         143  
322 58         129 my $b = $self->block($bname);
323 58         2166 foreach my $n ($b->names_str) {
324 222   50     639 $vmblocks->{$n} = $vm->{name}//1;
325             }
326             }
327             }
328 7         35 foreach my $p (keys %$partitions) {
329 50         115 my $b = $self->block($p);
330 50 100       1305 if (exists($vmblocks->{$b->name})) {
331 29         206 $self->log({level=>1}, "Removing ".$b->dname." (in VM ".$vmblocks->{$b->name}.")");
332 29         298 delete($partitions->{$p});
333             }
334             }
335             }
336              
337             sub createPartitionTables {
338 7     7 0 19 my $self = shift;
339 7         16 my $root = shift;
340 7         32 $self->log("Creating partition tables");
341 7 50       40 if (defined($self->get_info('partitions'))) {
342 7         14 foreach my $p (sort keys %{$self->get_info('partitions')}) {
  7         25  
343 21 50       143 next if defined($self->get_info('partitions', $p, 'dos-extended'));
344 21         135 $self->createPartitionTable($root, $p);
345             }
346             }
347             }
348              
349             sub createPartitionTable {
350 21     21 0 48 my $self = shift;
351 21         41 my $root = shift;
352 21         47 my $dev = shift;
353              
354 21         103 my $block = $self->block($dev);
355 21         120 my $elem;
356              
357 21         964 my $pttype = $block->blk_info("PTTYPE");
358 21   33     89 $pttype //= $self->get_info('partitions', $dev, 'type');
359 21 50       81 if (! defined($pttype)) {
360 0         0 $self->error("Unkown partition table for ".$block->name);
361 0         0 return;
362             }
363              
364 21 100 33     84 if ($pttype eq "gpt") {
    50          
365 19         138 $elem = $root->newChild('Partition::GPT', $block, $self, @_);
366             } elsif ($pttype eq "dos" || $pttype eq "msdos") {
367 2         25 $elem = $root->newChild('Partition::MSDOS', $block, $self, @_);
368             } else {
369 0         0 $self->warn("Unknown partition type ".$pttype." for ".$block->name);
370 0         0 return;
371             }
372 21 50       205 if (!$self->_registerElement($elem)) {
373 0         0 $self->error("Cannot register partition table for ".$block->name);
374 0         0 return;
375             }
376             }
377              
378             sub createEmptyDisks {
379 7     7 0 20 my $self = shift;
380 7         19 my $root = shift;
381              
382 7         32 $self->log("Creating disks without partitions");
383 7 100       46 if (defined($self->get_info('disks-no-part'))) {
384 5         15 foreach my $p (sort keys %{$self->get_info('disks-no-part')}) {
  5         19  
385 16         92 $self->createEmptyDisk($root, $p);
386             }
387             }
388             }
389              
390             sub createEmptyDisk {
391 16     16 0 49 my $self = shift;
392 16         30 my $root = shift;
393 16         32 my $dev = shift;
394              
395 16         84 my $block = $self->block($dev);
396 16         87 my $elem;
397              
398 16 100       104 if ($block->provided) {
399 13         63 $self->log(" skipping $dev already created");
400 13         66 return;
401             }
402              
403 3         19 $elem = $root->newChild('Partition::None', $block, $self, @_);
404 3 50       17 if (!$self->_registerElement($elem)) {
405 0         0 $self->error("Cannot register empty disk for ".$block->name);
406 0         0 return;
407             }
408             }
409              
410             sub createLVMs {
411 7     7 0 23 my $self = shift;
412 7         18 my $root = shift;
413              
414 7         49 $self->log('Creating LVM volume groups');
415 7   50     31 for my $vgname (sort keys %{$self->get_info('lvm') // {}}) {
  7         44  
416 9         25 my $elem;
417 9 50       44 if ($vgname eq '') {
418 0         0 $elem = $root->newChild('LVM::UnassignedPVs', $vgname, $self);
419             } else {
420 9         74 $elem = $root->newChild('LVM::VG', $vgname, $self);
421             }
422 9 50       92 if (!$self->_registerElement($elem)) {
423 0         0 $self->error("Cannot register LVM vg ".$vgname);
424 0         0 return;
425             }
426             }
427             }
428              
429             sub createLUKSs {
430 7     7 0 39 my $self = shift;
431 7         16 my $root = shift;
432              
433 7         52 $self->log("Creating LUKS devices");
434 7   100     30 for my $devname (sort keys %{$self->get_info('luks') // {}}) {
  7         45  
435 5         21 my $elem = $root->newChild('LUKS', $devname, $self);
436 5 50       28 if (!$self->_registerElement($elem)) {
437 0         0 $self->error("Cannot register LUKS device ".$devname);
438 0         0 return;
439             }
440             }
441             }
442              
443             sub createMDs {
444 7     7 0 24 my $self = shift;
445 7         18 my $root = shift;
446              
447 7         35 $self->log("Creating MD devices");
448 7   100     26 for my $devname (sort keys %{$self->get_info('md') // {}}) {
  7         34  
449 8         24 my $elem;
450 8 50 50     39 if ($self->get_info('md')->{$devname}->{'raid-container'} // 0 eq 1) {
451 0         0 $elem = $root->newChild('RAID::MD::Container', $devname, $self);
452             } else {
453 8         73 $elem = $root->newChild('RAID::MD', $devname, $self);
454             }
455 8 50       60 if (!$self->_registerElement($elem)) {
456 0         0 $self->error("Cannot register MD device ".$devname);
457 0         0 return;
458             }
459             }
460             }
461              
462             sub createLSIMegaclis {
463 7     7 0 18 my $self = shift;
464 7         18 my $root = shift;
465              
466 7         38 $self->log("Creating Megacli controllers");
467 7   100     31 for my $cnum (sort keys %{$self->get_info('lsi-megacli') // {}}) {
  7         656  
468 1         8 my $elem = $root->newChild('RAID::LSI::Megacli', $cnum, $self);
469 1 50       8 if (!$self->_registerElement($elem)) {
470 0         0 $self->error("Cannot register Megacli controller #".$cnum);
471 0         0 return;
472             }
473             }
474             }
475              
476             sub createLSISASIrcus {
477 7     7 0 18 my $self = shift;
478 7         18 my $root = shift;
479              
480 7         51 $self->log("Creating SAS LSI controllers");
481 7   100     53 for my $cnum (sort keys %{$self->get_info('lsi-sas-ircu') // {}}) {
  7         577  
482 1         6 my $elem = $root->newChild('RAID::LSI::SASIrcu', $cnum, $self);
483 1 50       8 if (!$self->_registerElement($elem)) {
484 0         0 $self->error("Cannot register SAS LSI controller #".$cnum);
485 0         0 return;
486             }
487             }
488             }
489              
490             sub createFSs {
491 7     7 0 19 my $self = shift;
492 7         18 my $root = shift;
493              
494 7         45 my $elem = $root->newChild('FS', $self);
495 7 50       56 if (!$self->_registerElement($elem)) {
496 0         0 print STDERR "Cannot register FS\n";
497 0         0 return;
498             }
499 7         24 return $elem;
500             }
501              
502             sub createVMs {
503 7     7 0 33 my $self = shift;
504 7         19 my $root = shift;
505              
506 7         38 my $elem = $root->newChild('Libvirt', $self);
507 7 50       78 if (!$self->_registerElement($elem)) {
508 0         0 print STDERR "Cannot register Libvirt\n";
509 0         0 return;
510             }
511 7         641 return $elem;
512             }
513              
514             sub createLoops {
515 7     7 0 23 my $self = shift;
516 7         18 my $root = shift;
517              
518 7         58 $self->log("Creating Loop devices");
519 7   50     50 for my $loop (sort keys %{$self->get_info('loops') // {}}) {
  7         46  
520 0         0 my $elem = $root->newChild('Loop', $loop, $self);
521 0 0       0 if (!$self->_registerElement($elem)) {
522 0         0 $self->error("Cannot register loop device ".$loop);
523 0         0 return;
524             }
525             }
526             }
527              
528             sub computeUsedBlocks {
529 7     7 0 18 my $self = shift;
530              
531 7         349 my $it = $self->elemsRoot->iterator(recurse => 1);
532 7         13063 while (defined(my $e=$it->next)) {
533             my @blocks = grep {
534 550         41260 $_->provided
  176         935  
535             } $e->consumedBlocks;
536              
537 550 100       2560 if (scalar(@blocks)>0) {
538 147         349 foreach my $block (@blocks) {
539 147         5811 $block->state("used");
540             #print STDERR "Block ", $block->name, " used due to ", $e->name, "\n";
541             }
542             }
543             #else {
544             # print STDERR "No providers for ",
545             # join(",",
546             # (map { $_->name } $e->consumedBlocks)), "\n";
547             #}
548             }
549              
550             }
551              
552             sub display {
553 7     7 0 22 my $self = shift;
554 7         53 print join("\n", $self->dotNode), "\n";
555             }
556              
557             has '_mountpoints' => (
558             is => 'ro',
559             isa => 'HashRef[Num]',
560             traits => [ 'Hash' ],
561             required => 1,
562             lazy => 1,
563             builder => '_compute_mp',
564             handles => {
565             'has_mp' => 'exists',
566             'fs_mountpoint_blockname' => 'get',
567             'fs_mountpoint_id' => 'get',
568             },
569             );
570              
571             sub _compute_mp {
572 7     7   612 my $self = shift;
573              
574 7         23 my $st = $self;
575 7         39 my $allfs = $st->get_info('fs', 'hierarchy');
576 7         37 my $flatfs = $st->get_info('fs', 'flatfull');
577 7         23 my $mp = {};
578 7         20 my $comp_rec;
579             $comp_rec = sub {
580 272     272   387 my $node = shift;
581 272         403 my $parent = shift;
582 272 50       1157 if (not defined($flatfs->{$node->{id}})) {
    100          
    50          
583 0         0 $self->warn($node->{id}.' not defined');
584             } elsif (not defined($flatfs->{$node->{id}}->{parent})) {
585 229         505 $flatfs->{$node->{id}}->{parent} = $parent;
586             } elsif ($flatfs->{$node->{id}}->{parent} != $parent) {
587             $self->warn('FS: wrong parent for '.$node->{target}
588             .' got: '.$flatfs->{$node->{id}}->{parent}
589 0         0 .' expects: '.$parent);
590             }
591 272 100       537 if ($parent != 1) {
592 265         365 push @{$flatfs->{$parent}->{children}}, $node->{id};
  265         735  
593             }
594 272         882 $mp->{$node->{target}} = $node->{id};
595 272 100       686 if (exists($node->{children})) {
596 62         703 for my $child (@{$node->{children}}) {
  62         170  
597 265         544 $comp_rec->($child, $node->{id});
598             }
599             }
600 7         71 };
601 7         34 $comp_rec->($allfs, 1);
602 7         435 return $mp;
603             }
604              
605             around 'fs_mountpoint_blockname' => sub {
606             my $orig = shift;
607             my $self = shift;
608             my $mountpoint = shift;
609              
610             # must return an unique (per machine) fake blockname
611             # for the provided mount point
612             if (not $self->has_mp($mountpoint)) {
613             $self->warn("No mountpoint for $mountpoint");
614             return 'FS@-1@'.$mountpoint;
615             }
616             my $id = $self->$orig($mountpoint, @_);
617              
618             return $self->fs_mountpoint_blockname_by_id($id, $mountpoint);
619             };
620              
621             sub fs_mountpoint_blockname_by_id {
622 313     313 0 658 my $self = shift;
623 313         632 my $id = shift;
624 313         594 my $mp = shift;
625              
626 313         1384 my $target = $self->get_info('fs', 'flatfull', $id)->{target};
627              
628 313 50 66     1878 if (defined($mp) and not ($target eq $mp)) {
629 0         0 $self->warn("Bad mountpoint: got $mp, expects $target");
630             }
631              
632 313         1593 return 'FS@'.$id.'@'.$target;
633             }
634              
635             sub fs_swap_blockname {
636             # must return an unique (per machine) fake blockname
637             # for the provided device/file swap
638 5     5 0 13 my $self = shift;
639 5         34 my $swappath = shift;
640              
641 5         28 return 'FS@SWAP@'.$swappath;
642             }
643              
644             # FIXME: to remove when StorageDisplay will be a StorageDisplay::Data::Elem
645             sub pushDotText {
646 7     7 0 41 my $self = shift;
647 7         18 my $text = shift;
648 7   50     40 my $t = shift // "\t";
649              
650 7         40 my @pushed = map { $t.$_ } @_;
  3933         8109  
651 7         42 push @{$text}, @pushed;
  7         921  
652             }
653              
654             sub dotNode {
655 7     7 0 22 my $self = shift;
656 7   50     67 my $t = shift // "\t";
657 7         386 my @text = map { $_." // HEADER: MACHINE"} (
  14         52  
658             'digraph "'.$self->elemsRoot->host.'"{',
659             $t."rankdir=LR;",
660             );
661 7         255 $self->pushDotText(\@text, $t, $self->elemsRoot->dotNode("$t"));
662 7         370 push @text, "} // FOOTER: MACHINE";
663              
664 7         3490 return @text;
665             }
666              
667             1;
668              
669             __END__
670              
671             =pod
672              
673             =encoding UTF-8
674              
675             =head1 NAME
676              
677             StorageDisplay - Collect and display storages on linux machines
678              
679             =head1 VERSION
680              
681             version 2.06
682              
683             =head1 AUTHOR
684              
685             Vincent Danjean <Vincent.Danjean@ens-lyon.org>
686              
687             =head1 COPYRIGHT AND LICENSE
688              
689             This software is copyright (c) 2014-2023 by Vincent Danjean.
690              
691             This is free software; you can redistribute it and/or modify it under
692             the same terms as the Perl 5 programming language system itself.
693              
694             =cut