File Coverage

blib/lib/Net/SNMP/Mixin/CiscoDot1qVlanStatic.pm
Criterion Covered Total %
statement 72 155 46.4
branch 28 66 42.4
condition 3 6 50.0
subroutine 18 21 85.7
pod 3 3 100.0
total 124 251 49.4


line stmt bran cond sub pod time code
1             package Net::SNMP::Mixin::CiscoDot1qVlanStatic;
2              
3 4     4   489015 use strict;
  4         11  
  4         124  
4 4     4   27 use warnings;
  4         11  
  4         157  
5              
6             #
7             # store this package name in a handy variable,
8             # used for unambiguous prefix of mixin attributes
9             # storage in object hash
10             #
11             my $prefix = __PACKAGE__;
12              
13             #
14             # this module import config
15             #
16 4     4   25 use Carp ();
  4         11  
  4         97  
17              
18 4     4   22 use Net::SNMP::Mixin::Util qw/idx2val hex2octet push_error get_init_slot/;
  4         19  
  4         34  
19              
20             #
21             # this module export config
22             #
23             my @mixin_methods;
24              
25             BEGIN {
26 4     4   2246 @mixin_methods = qw/
27             map_vlan_id2name
28             map_vlan_id2if_idx
29             map_if_idx2vlan_id
30             /;
31             }
32              
33 4         65 use Sub::Exporter -setup => {
34             exports => [@mixin_methods],
35             groups => { default => [@mixin_methods], },
36 4     4   31 };
  4         9  
37              
38             #
39             # SNMP oid constants from CISCO-VTP-MIB and CISCO-VLAN-MEMBERSHIP-MIB
40             #
41             use constant {
42             ###
43             # trunk ports: CISCO-VTP-MIB
44 4         1754 VLAN_TBL => '1.3.6.1.4.1.9.9.46.1.3.1',
45             VLAN_STATE => '1.3.6.1.4.1.9.9.46.1.3.1.1.2',
46             VLAN_NAME => '1.3.6.1.4.1.9.9.46.1.3.1.1.4',
47              
48             VLAN_TRUNK_PORT_TBL => '1.3.6.1.4.1.9.9.46.1.6.1',
49             VLAN_TRUNK_PORT_VLANS_ENABLED_1K => '1.3.6.1.4.1.9.9.46.1.6.1.1.4',
50             VLAN_TRUNK_PORT_NATIVE_VLAN => '1.3.6.1.4.1.9.9.46.1.6.1.1.5',
51             VLAN_TRUNK_PORT_ENCAPS_OPER_TYPE => '1.3.6.1.4.1.9.9.46.1.6.1.1.16',
52             VLAN_TRUNK_PORT_VLANS_ENABLED_2K => '1.3.6.1.4.1.9.9.46.1.6.1.1.17',
53             VLAN_TRUNK_PORT_VLANS_ENABLED_3K => '1.3.6.1.4.1.9.9.46.1.6.1.1.18',
54             VLAN_TRUNK_PORT_VLANS_ENABLED_4K => '1.3.6.1.4.1.9.9.46.1.6.1.1.19',
55              
56             ###
57             # access ports: CISCO-VLAN-MEMBERSHIP-MIB
58             VM_MEMBERSHIP_TABLE => '1.3.6.1.4.1.9.9.68.1.2.2',
59             VM_VLAN_TYPE => '1.3.6.1.4.1.9.9.68.1.2.2.1.1',
60             VM_VLAN => '1.3.6.1.4.1.9.9.68.1.2.2.1.2',
61 4     4   2338 };
  4         26  
62              
63             =head1 NAME
64              
65             Net::SNMP::Mixin::CiscoDot1qVlanStatic - mixin class for static Cisco vlan info
66              
67             =head1 VERSION
68              
69             Version 0.01
70              
71             =cut
72              
73             our $VERSION = '0.01';
74              
75             =head1 SYNOPSIS
76              
77             use Net::SNMP;
78             use Net::SNMP::Mixin;
79              
80             # initialize session and mixin library
81             my $session = Net::SNMP->session( -hostname => 'foo.bar.com' );
82             $session->mixer('Net::SNMP::Mixin::CiscoDot1qVlanStatic');
83             $session->init_mixins;
84             snmp_dispatcher();
85             $session->init_ok();
86             die $session->errors if $session->errors;
87              
88             # show VLAN IDs and corresponding names
89             my $id2name = $session - map_vlan_id2name();
90             foreach my $id ( keys %{$id2name} ) {
91             printf "Vlan-Id: %4d => Vlan-Name: %s\n", $id, $id2name->{$id};
92             }
93              
94             # sorted by vlan_id
95             my $vlan_ids2if_idx = $session->map_vlan_id2if_idx();
96             foreach my $id ( keys %{$vlan_ids2if_idx} ) {
97             printf "Vlan-Id: %4d\n", $id;
98             printf "\tTagged-Ports: %s\n", ( join ',', @{ $vlan_ids2if_idx->{$id}{tagged} } );
99             printf "\tUntagged-Ports: %s\n", ( join ',', @{ $vlan_ids2if_idx->{$id}{untagged} } );
100             }
101              
102             # sorted by interface
103             my $ports2ids = $session->map_if_idx2vlan_id();
104             foreach my $if_idx ( keys %{$ports2ids} ) {
105             printf "Interface: %10d\n", $if_idx;
106             printf "\tTagged-Vlans: %s\n", ( join ',', @{ $ports2ids->{$if_idx}{tagged} } );
107             printf "\tUntagged-Vlans: %s\n", ( join ',', @{ $ports2ids->{$if_idx}{untagged} } );
108             }
109              
110             =head1 DESCRIPTION
111              
112             A mixin class for vlan related infos from the CISCO-VTP-MIB for 802.1Q-trunks and
113             CISCO-VLAN-MEMBERSHIP-MIB for access ports.
114             The mixin-module provides methods for mapping between vlan-ids and vlan-names und relations between
115             interfaces and vlan-ids, tagged or untagged on these ports.
116              
117             =head1 MIXIN METHODS
118              
119             =head2 B<< OBJ->map_vlan_id2name() >>
120              
121             Returns a hash reference with vlan-ids as keys and the corresponding vlan-names as values:
122              
123             {
124             vlan_id => vlan_name,
125             vlan_id => vlan_name,
126             ... ,
127             }
128              
129             =cut
130              
131             sub map_vlan_id2name {
132 1     1 1 30488 my $session = shift;
133             Carp::croak "'$prefix' not initialized,"
134 1 50       83 unless $session->{$prefix}{__initialized};
135              
136 0         0 return $session->{$prefix}{vlan_id2name};
137             }
138              
139             =head2 B<< OBJ->map_vlan_id2if_idx() >>
140              
141             Returns a hash reference with the vlan-ids as keys and tagged and untagged if_idx as values:
142              
143             {
144             vlan_id => {
145             tagged => [if_idx, ..., ],
146             untagged => [if_idx, ..., ],
147             },
148              
149             ... ,
150             }
151            
152             =cut
153              
154             sub map_vlan_id2if_idx {
155 1     1 1 601 my $session = shift;
156             Carp::croak "'$prefix' not initialized,"
157 1 50       83 unless $session->{$prefix}{__initialized};
158              
159 0         0 return _get_vlan_ids2if_idx($session);
160             }
161              
162             =head2 B<< OBJ->map_if_idx2vlan_id() >>
163              
164             Returns a hash reference with the interfaces as keys and tagged and untagged vlan-ids as values:
165              
166             {
167             if_idx => {
168             tagged => [vlan_id, ..., ],
169             untagged => [vlan_id, ..., ],
170             },
171              
172             ... ,
173             }
174            
175             =cut
176              
177             sub map_if_idx2vlan_id {
178 1     1 1 592 my $session = shift;
179             Carp::croak "'$prefix' not initialized,"
180 1 50       134 unless $session->{$prefix}{__initialized};
181              
182 0         0 return _get_if_idx2vlan_ids($session);
183             }
184              
185             =head1 INITIALIZATION
186              
187             =head2 B<< OBJ->_init($reload) >>
188              
189             Fetch basic Vlan related SNMP values from the host. Don't call this method direct!
190              
191             =cut
192              
193             #
194             # due to the asynchron nature, we don't know what init job is really the last, we decrement
195             # the value after each callback
196             #
197 4     4   34 use constant THIS_INIT_JOBS => 3;
  4         11  
  4         7046  
198              
199             sub _init {
200 4     4   12495 my ( $session, $reload ) = @_;
201              
202 4         14 my $agent = $session->hostname;
203              
204             die "$agent: $prefix already initialized and reload not forced.\n"
205             if exists get_init_slot($session)->{$prefix}
206 4 50 66     34 && get_init_slot($session)->{$prefix} == 0
      33        
207             && not $reload;
208              
209             # set number of async init jobs for proper initialization
210 4         128 get_init_slot($session)->{$prefix} = THIS_INIT_JOBS;
211              
212             # initialize the object for vtp vlan table
213 4         45 _fetch_vtp_vlan_tbl_entries($session);
214 4 100       46 return if $session->error;
215              
216             # initialize the object for vtp vlan trunk port table for trunk ports
217 2         17 _fetch_vtp_vlan_trunk_port_tbl_entries($session);
218 2 50       15 return if $session->error;
219              
220             # initialize the object for vlan membership table of access ports
221 2         20 _fetch_vm_membership_tbl_entries($session);
222 2 50       14 return if $session->error;
223              
224 2         17 return 1;
225             }
226              
227             =head1 PRIVATE METHODS
228              
229             Only for developers or maintainers.
230              
231             =head2 B<< _fetch_vtp_vlan_tbl_entries($session) >>
232              
233             Fetch selected rows from vtpVlanTable during object initialization.
234              
235             =cut
236              
237             sub _fetch_vtp_vlan_tbl_entries {
238 4     4   10 my $session = shift;
239 4         19 my $result;
240              
241             # fetch the vlan state and name from vlanTable
242 4 100       28 $result = $session->get_entries(
    50          
243             -columns => [ VLAN_STATE, VLAN_NAME, ],
244              
245             # define callback if in nonblocking mode
246             $session->nonblocking
247             ? ( -callback => \&_vtp_vlan_tbl_entries_cb )
248             : (),
249              
250             # dangerous for snmp version 2c and 3, big values
251             # snmp-error: Message size exceeded buffer maxMsgSize
252             #
253             $session->version ? ( -maxrepetitions => 3 ) : (),
254             );
255              
256 4 100       2011885 return unless defined $result;
257 2 50       27 return 1 if $session->nonblocking;
258              
259             # call the callback function in blocking mode by hand
260 0         0 _vtp_vlan_tbl_entries_cb($session);
261              
262             }
263              
264             =head2 B<< _vtp_vlan_tbl_entries_cb($session) >>
265              
266             The callback for _fetch_vtp_vlan_tbl_entries.
267              
268             =cut
269              
270             sub _vtp_vlan_tbl_entries_cb {
271 2     2   2005881 my $session = shift;
272 2         41 my $vbl = $session->var_bind_list;
273              
274 2 50       35 unless ( defined $vbl ) {
275 2 50       15 if ( my $err_msg = $session->error ) {
276 2         43 push_error( $session, "$prefix: $err_msg" );
277             }
278 2         95 return;
279             }
280              
281             # mangle result table to get plain
282             # VlanIndex => vlan-state
283             #
284 0         0 $session->{$prefix}{_VlanState} = idx2val( $vbl, VLAN_STATE, 1 );
285              
286             # mangle result table to get plain
287             # VlanIndex => vlan-name
288             #
289 0         0 $session->{$prefix}{vlan_id2name} = idx2val( $vbl, VLAN_NAME, 1 );
290              
291             # purge non operational vlans, see CISCO-VTP-MIB
292 0         0 foreach my $vlan_id ( keys %{ $session->{$prefix}{vlan_id2name} } ) {
  0         0  
293             delete $session->{$prefix}{vlan_id2name}{$vlan_id}
294 0 0       0 unless $session->{$prefix}{_VlanState}{$vlan_id} == 1;
295             }
296              
297             # this init job is finished
298 0         0 get_init_slot($session)->{$prefix}--;
299              
300 0         0 return 1;
301             }
302              
303             =head2 B<< _fetch_vtp_vlan_trunk_port_tbl_entries($session) >>
304              
305             Fetch selected rows from vlanTrunkPortTable during object initialization.
306              
307             =cut
308              
309             sub _fetch_vtp_vlan_trunk_port_tbl_entries {
310 2     2   6 my $session = shift;
311 2         5 my $result;
312              
313             # fetch selected entries from vlanTrunkPortTable
314 2 50       13 $result = $session->get_entries(
    50          
315             -columns => [
316             VLAN_TRUNK_PORT_ENCAPS_OPER_TYPE,
317             VLAN_TRUNK_PORT_NATIVE_VLAN,
318              
319             VLAN_TRUNK_PORT_VLANS_ENABLED_1K,
320             VLAN_TRUNK_PORT_VLANS_ENABLED_2K,
321             VLAN_TRUNK_PORT_VLANS_ENABLED_3K,
322             VLAN_TRUNK_PORT_VLANS_ENABLED_4K,
323             ],
324              
325             # define callback if in nonblocking mode
326             $session->nonblocking ? ( -callback => \&_vtp_vlan_trunk_port_tbl_entries_cb ) : (),
327              
328             # dangerous for snmp version 2c and 3, big values
329             # snmp-error: Message size exceeded buffer maxMsgSize
330             #
331             $session->version ? ( -maxrepetitions => 3 ) : (),
332             );
333              
334 2 50       3479 return unless defined $result;
335 2 50       13 return 1 if $session->nonblocking;
336              
337             # call the callback function in blocking mode by hand
338 0         0 _vtp_vlan_trunk_port_tbl_entries_cb($session);
339              
340             }
341              
342             =head2 B<< _vtp_vlan_trunk_port_tbl_entries_cb($session) >>
343              
344             The callback for _fetch_vtp_vlan_trunk_port_tbl_entries.
345              
346             =cut
347              
348             sub _vtp_vlan_trunk_port_tbl_entries_cb {
349 2     2   1153 my $session = shift;
350 2         13 my $vbl = $session->var_bind_list;
351              
352 2 50       25 unless ( defined $vbl ) {
353 2 50       10 if ( my $err_msg = $session->error ) {
354 2         23 push_error( $session, "$prefix: $err_msg" );
355             }
356 2         99 return;
357             }
358              
359             # mangle result table to get plain
360             # ifIndex => vlans-enabled-bitstring
361             #
362             $session->{$prefix}{_VlansEnabled1k} =
363 0         0 idx2val( $vbl, VLAN_TRUNK_PORT_VLANS_ENABLED_1K, );
364              
365             $session->{$prefix}{_VlansEnabled2k} =
366 0         0 idx2val( $vbl, VLAN_TRUNK_PORT_VLANS_ENABLED_2K, );
367              
368             $session->{$prefix}{_VlansEnabled3k} =
369 0         0 idx2val( $vbl, VLAN_TRUNK_PORT_VLANS_ENABLED_3K, );
370              
371             $session->{$prefix}{_VlansEnabled4k} =
372 0         0 idx2val( $vbl, VLAN_TRUNK_PORT_VLANS_ENABLED_4K, );
373              
374             $session->{$prefix}{_VlansEncapsOperType} =
375 0         0 idx2val( $vbl, VLAN_TRUNK_PORT_ENCAPS_OPER_TYPE, );
376              
377             $session->{$prefix}{NativeVlan} =
378 0         0 idx2val( $vbl, VLAN_TRUNK_PORT_NATIVE_VLAN, );
379              
380 0         0 $session->{$prefix}{__initialized}++;
381              
382 0         0 _calc_tagged_ports($session);
383              
384             # this init job is finished
385 0         0 get_init_slot($session)->{$prefix}--;
386              
387 0         0 return 1;
388             }
389              
390             sub _calc_tagged_ports {
391 0     0   0 my $session = shift;
392              
393             # prepare fillmask, see below
394 0         0 my $zeroes_1k = pack( 'B*', 0 x 1024 );
395              
396             # iterate over any hash to get the interfaces as keys
397 0         0 foreach my $if_idx ( keys %{ $session->{$prefix}{NativeVlan} } ) {
  0         0  
398              
399             # only dot1Q(4) is supported, see CISCO-VTP-MIB
400 0 0       0 if ( $session->{$prefix}{_VlansEncapsOperType}{$if_idx} != 4 ) {
401 0         0 $session->{$prefix}{TaggedVlans}{$if_idx} = undef;
402 0         0 next;
403             }
404              
405             # for all phys interfaces get the tagged vlans
406             # represented in OCTET-STRINGS
407              
408 0         0 my $vlans_1k = $session->{$prefix}{_VlansEnabled1k}{$if_idx};
409 0         0 my $vlans_2k = $session->{$prefix}{_VlansEnabled2k}{$if_idx};
410 0         0 my $vlans_3k = $session->{$prefix}{_VlansEnabled3k}{$if_idx};
411 0         0 my $vlans_4k = $session->{$prefix}{_VlansEnabled4k}{$if_idx};
412              
413             # It's important that the returned SNMP OCTET-STRINGs were untranslated by Net::SNMP!
414             # If already translated, we must convert it back to a pure OCTET-STRING
415             # and fill it with zeroes to a length of 128-OCTETS = 1024-BITS
416              
417 0         0 my $vlans_1k_octets = hex2octet($vlans_1k) ^ $zeroes_1k;
418 0         0 my $vlans_2k_octets = hex2octet($vlans_2k) ^ $zeroes_1k;
419 0         0 my $vlans_3k_octets = hex2octet($vlans_3k) ^ $zeroes_1k;
420 0         0 my $vlans_4k_octets = hex2octet($vlans_4k) ^ $zeroes_1k;
421              
422             # unpack it into a bit-string
423 0         0 my $vlans_1k_bits = unpack( 'B*', $vlans_1k_octets );
424 0         0 my $vlans_2k_bits = unpack( 'B*', $vlans_2k_octets );
425 0         0 my $vlans_3k_bits = unpack( 'B*', $vlans_3k_octets );
426 0         0 my $vlans_4k_bits = unpack( 'B*', $vlans_4k_octets );
427              
428             # concat all 4k possible vlan_ids as bitstring
429 0         0 $session->{$prefix}{TaggedVlans}{$if_idx} =
430             $vlans_1k_bits . $vlans_2k_bits . $vlans_3k_bits . $vlans_4k_bits;
431             }
432              
433             }
434              
435             =head2 B<< _fetch_vm_membership_tbl_entries($session) >>
436              
437             Fetch selected rows from vmMembershipTable during object initialization.
438              
439             =cut
440              
441             sub _fetch_vm_membership_tbl_entries {
442 2     2   6 my $session = shift;
443 2         5 my $result;
444              
445             # fetch selected entries from vmMembershipTable
446 2 50       12 $result = $session->get_entries(
    50          
447             -columns => [ VM_VLAN_TYPE, VM_VLAN ],
448              
449             # define callback if in nonblocking mode
450             $session->nonblocking ? ( -callback => \&_vm_membership_tbl_entries_cb ) : (),
451              
452             # dangerous for snmp version 2c and 3, big values
453             # snmp-error: Message size exceeded buffer maxMsgSize
454             #
455             $session->version ? ( -maxrepetitions => 3 ) : (),
456             );
457              
458 2 50       2340 return unless defined $result;
459 2 50       9 return 1 if $session->nonblocking;
460              
461             # call the callback function in blocking mode by hand
462 0         0 _vm_membership_tbl_entries_cb($session);
463              
464             }
465              
466             =head2 B<< _vm_membership_tbl_entries_cb($session) >>
467              
468             The callback for _fetch_vm_membership_tbl_entries
469              
470             =cut
471              
472             sub _vm_membership_tbl_entries_cb {
473 2     2   1085 my $session = shift;
474 2         9 my $vbl = $session->var_bind_list;
475              
476 2 50       24 unless ( defined $vbl ) {
477 2 50       8 if ( my $err_msg = $session->error ) {
478 2         32 push_error( $session, "$prefix: $err_msg" );
479             }
480 2         58 return;
481             }
482              
483             # mangle result table to get plain
484             # ifIndex => values
485             #
486             $session->{$prefix}{_VlanType} =
487 0           idx2val( $vbl, VM_VLAN_TYPE, );
488              
489             $session->{$prefix}{_VlanId} =
490 0           idx2val( $vbl, VM_VLAN, );
491              
492 0           foreach my $if_idx ( keys %{ $session->{$prefix}{_VlanType} } ) {
  0            
493              
494             # only static(1) vlans are supported, see CISCO-VLAN-MEMBERSHIP-MIB
495 0 0         next if $session->{$prefix}{_VlanType}{$if_idx} != 1;
496              
497 0           $session->{$prefix}{AccessVlan}{$if_idx} = $session->{$prefix}{_VlanId}{$if_idx};
498             }
499              
500             # this init job is finished
501 0           get_init_slot($session)->{$prefix}--;
502              
503 0           return 1;
504             }
505              
506             # Process tagged/untagged ports for each vlan
507             sub _get_vlan_ids2if_idx {
508 0     0     my $session = shift;
509              
510 0           my $result;
511 0           foreach my $vlan_id ( sort keys %{ $session->{$prefix}{vlan_id2name} } ) {
  0            
512 0           $result->{$vlan_id}{tagged} = [];
513 0           $result->{$vlan_id}{untagged} = [];
514              
515             # iterate over any hash from VTP table to get all interfaces
516 0           foreach my $if_idx ( sort keys %{ $session->{$prefix}{NativeVlan} } ) {
  0            
517              
518             # access ports
519 0 0         if ( my $access_vlan = $session->{$prefix}{AccessVlan}{$if_idx} ) {
520 0 0         push( @{ $result->{$vlan_id}{untagged} }, $if_idx ) if $access_vlan == $vlan_id;
  0            
521              
522             # next interface
523 0           next;
524             }
525              
526             # trunk ports
527 0 0         next unless defined $session->{$prefix}{TaggedVlans}{$if_idx};
528              
529 0 0         if ( substr( $session->{$prefix}{TaggedVlans}{$if_idx}, $vlan_id, 1 ) eq 1 ) {
530 0 0         if ( $session->{$prefix}{NativeVlan}{$if_idx} != $vlan_id ) {
531              
532             # ... and it's not the native vlan of this trunk
533 0           push @{ $result->{$vlan_id}{tagged} }, $if_idx;
  0            
534             }
535             else {
536             # ... it's the native vlan of this trunk
537 0           push @{ $result->{$vlan_id}{untagged} }, $if_idx;
  0            
538             }
539             }
540             }
541             }
542              
543 0           return $result;
544             }
545              
546             # Process tagged/untagged vlans for each interface
547             #
548             # reverse datastructure vlan_ids to ports ==> ports to vlan ids
549             #
550             # FROM:
551             # vlan_id => {
552             # tagged => [if_idx, ..., ],
553             # untagged => [if_idx, ..., ],
554             # },
555             #
556             # TO:
557             # if_idx => {
558             # tagged => [vlan_id, ..., ],
559             # untagged => [vlan_id, ..., ],
560             # },
561             #
562             sub _get_if_idx2vlan_ids {
563 0     0     my $vlan_ids2if_idx = _get_vlan_ids2if_idx(shift);
564              
565 0           my $result;
566 0           foreach my $vlan_id ( keys %$vlan_ids2if_idx ) {
567 0           foreach my $if_idx ( @{ $vlan_ids2if_idx->{$vlan_id}{tagged} } ) {
  0            
568 0           push @{ $result->{$if_idx}{tagged} }, $vlan_id;
  0            
569             }
570 0           foreach my $if_idx ( @{ $vlan_ids2if_idx->{$vlan_id}{untagged} } ) {
  0            
571 0           push @{ $result->{$if_idx}{untagged} }, $vlan_id;
  0            
572             }
573             }
574 0           return $result;
575             }
576              
577             =head1 REQUIREMENTS
578              
579             L<< Net::SNMP >>, L<< Net::SNMP::Mixin >>
580              
581             =head1 BUGS, PATCHES & FIXES
582              
583             There are no known bugs at the time of this release. However, if you spot a bug or are experiencing difficulties that are not explained within the POD documentation, please submit a bug to the RT system (see link below). However, it would help greatly if you are able to pinpoint problems or even supply a patch.
584              
585             Fixes are dependant upon their severity and my availablity. Should a fix not be forthcoming, please feel free to (politely) remind me by sending an email to gaissmai@cpan.org .
586              
587             RT: http://rt.cpan.org/Public/Dist/Display.html?Name=Net-SNMP-Mixin-CiscoDot1qVlanStatic
588              
589             =head1 AUTHOR
590              
591             Karl Gaissmaier
592              
593             =head1 COPYRIGHT & LICENSE
594              
595             Copyright 2020 Karl Gaissmaier, all rights reserved.
596              
597             This program is free software; you can redistribute it and/or modify it
598             under the same terms as Perl itself.
599              
600             =cut
601              
602             unless ( caller() ) {
603             print "$prefix compiles and initializes successful.\n";
604             }
605              
606             1;
607              
608             # vim: sw=2