File Coverage

blib/lib/Net/CLI/Interact/Phrasebook.pm
Criterion Covered Total %
statement 15 135 11.1
branch 0 52 0.0
condition 0 15 0.0
subroutine 5 20 25.0
pod 6 8 75.0
total 26 230 11.3


line stmt bran cond sub pod time code
1             package Net::CLI::Interact::Phrasebook;
2             { $Net::CLI::Interact::Phrasebook::VERSION = '2.300005' }
3              
4 1     1   8 use Moo;
  1         2  
  1         8  
5 1     1   333 use MooX::Types::MooseLike::Base qw(InstanceOf Str Any HashRef);
  1         2  
  1         87  
6              
7 1     1   490 use Path::Class;
  1         38350  
  1         68  
8 1     1   545 use File::ShareDir 'dist_dir';
  1         21554  
  1         69  
9 1     1   500 use Net::CLI::Interact::ActionSet;
  1         4  
  1         2177  
10              
11             has 'logger' => (
12             is => 'ro',
13             isa => InstanceOf['Net::CLI::Interact::Logger'],
14             required => 1,
15             );
16              
17             has 'personality' => (
18             is => 'rw',
19             isa => Str,
20             required => 1,
21             );
22              
23             has 'library' => (
24             is => 'lazy',
25             isa => Any, # FIXME 'Str|ArrayRef[Str]',
26             );
27              
28             sub _build_library {
29 0     0     return [ Path::Class::Dir->new( dist_dir('Net-CLI-Interact') )
30             ->subdir('phrasebook')->stringify ];
31             }
32              
33             has 'add_library' => (
34             is => 'rw',
35             isa => Any, # FIXME 'Str|ArrayRef[Str]',
36             default => sub { [] },
37             );
38              
39             has '_prompt' => (
40             is => 'ro',
41             isa => HashRef[InstanceOf['Net::CLI::Interact::ActionSet']],
42             default => sub { {} },
43             );
44              
45             sub prompt {
46 0     0 1   my ($self, $name) = @_;
47 0 0         die "unknown prompt [$name]" unless $self->has_prompt($name);
48 0           return $self->_prompt->{$name};
49             }
50              
51 0     0 1   sub prompt_names { return keys %{ (shift)->_prompt } }
  0            
52              
53             sub has_prompt {
54 0     0 1   my ($self, $name) = @_;
55 0 0 0       die "missing prompt name!"
56             unless defined $name and length $name;
57 0           return exists $self->_prompt->{$name};
58             }
59              
60             has '_macro' => (
61             is => 'ro',
62             isa => HashRef[InstanceOf['Net::CLI::Interact::ActionSet']],
63             default => sub { {} },
64             );
65              
66             sub macro {
67 0     0 1   my ($self, $name) = @_;
68 0 0         die "unknown macro [$name]" unless $self->has_macro($name);
69 0           return $self->_macro->{$name};
70             }
71              
72 0     0 1   sub macro_names { return keys %{ (shift)->_macro } }
  0            
73              
74             sub has_macro {
75 0     0 1   my ($self, $name) = @_;
76 0 0 0       die "missing macro name!"
77             unless defined $name and length $name;
78 0           return exists $self->_macro->{$name};
79             }
80              
81             # matches which are prompt names are resolved to RegexpRefs
82             # and regexp provided by the user are inflated into RegexpRefs
83             sub _resolve_matches {
84 0     0     my ($self, $actions) = @_;
85              
86 0           foreach my $a (@$actions) {
87 0 0         next unless $a->{type} eq 'match';
88 0 0         next unless ref $a->{value} eq ref [];
89              
90 0           my @newvals = ();
91 0           foreach my $v (@{ $a->{value} }) {
  0            
92 0 0 0       if ($v =~ m{^/} and $v =~ m{/$}) {
93 0           $v =~ s{^/}{}; $v =~ s{/$}{};
  0            
94 0           push @newvals, qr/$v/;
95             }
96             else {
97 0           push @newvals, @{ $self->prompt($v)->first->value };
  0            
98             }
99             }
100              
101 0           $a->{value} = \@newvals;
102             }
103              
104 0           return $actions;
105             }
106              
107             # inflate the hashref into action objects
108             sub _bake {
109 0     0     my ($self, $data) = @_;
110              
111 0 0 0       return unless ref $data eq ref {} and keys %$data;
112 0           $self->logger->log('phrasebook', 'debug', 'storing', $data->{type}, $data->{name});
113              
114 0           my $slot = '_'. lc $data->{type};
115             $self->$slot->{$data->{name}}
116             = Net::CLI::Interact::ActionSet->new({
117             actions => $self->_resolve_matches($data->{actions})
118 0           });
119             }
120              
121             sub BUILD {
122 0     0 0   my $self = shift;
123 0           $self->load_phrasebooks;
124             }
125              
126             # parse phrasebook files and load action objects
127             sub load_phrasebooks {
128 0     0 0   my $self = shift;
129 0           my $data = {};
130 0           my $stash = { prompt => [], macro => [] };
131              
132 0           foreach my $file ($self->_find_phrasebooks) {
133 0           $self->logger->log('phrasebook', 'info', 'reading phrasebook', $file);
134 0           my @lines = $file->slurp;
135 0           while ($_ = shift @lines) {
136             # Skip comments and empty lines
137 0 0         next if m/^(?:#|\s*$)/;
138              
139 0 0         if (m{^(prompt|macro)\s+(\w+)\s*$}) {
    0          
140 0 0         if (scalar keys %$data) {
141 0           push @{ $stash->{$data->{type}} }, $data;
  0            
142             }
143 0           $data = {type => $1, name => $2};
144 0           next;
145             }
146             # skip new sections we don't yet understand
147             elsif (m{^\w}) {
148 0           $_ = shift @lines until m{^(?:prompt|macro)};
149 0           unshift @lines, $_;
150 0           next;
151             }
152              
153 0 0         if (m{^\s+send\s+(.+)$}) {
154 0           my $value = $1;
155 0           $value =~ s/^["']//; $value =~ s/["']$//;
  0            
156 0           push @{ $data->{actions} }, {
  0            
157             type => 'send', value => $value,
158             };
159 0           next;
160             }
161              
162 0 0         if (m{^\s+put\s+(.+)$}) {
163 0           my $value = $1;
164 0           $value =~ s/^["']//; $value =~ s/["']$//;
  0            
165 0           push @{ $data->{actions} }, {
  0            
166             type => 'send', value => $value, no_ors => 1,
167             };
168 0           next;
169             }
170              
171 0 0         if (m{^\s+match\s+(.+)\s*$}) {
172 0           my @vals = split m/\s+or\s+/, $1;
173 0 0         if (scalar @vals) {
174 0           push @{ $data->{actions} },
  0            
175             {type => 'match', value => \@vals};
176 0           next;
177             }
178             }
179              
180 0 0         if (m{^\s+follow\s+/(.+)/\s+with\s+(.+)\s*$}) {
181 0           my ($match, $send) = ($1, $2);
182 0           $send =~ s/^["']//; $send =~ s/["']$//;
  0            
183             $data->{actions}->[-1]->{continuation} = [
184 0           {type => 'match', value => [qr/$match/]},
185             {type => 'send', value => eval "qq{$send}", no_ors => 1}
186             ];
187 0           next;
188             }
189              
190 0           die "don't know what to do with this phrasebook line:\n", $_;
191             }
192             # last entry in the file needs baking
193 0           push @{ $stash->{$data->{type}} }, $data;
  0            
194 0           $data = {};
195             }
196              
197             # bake the prompts before the macros, to allow macros to reference
198             # prompts which appear later in the same file.
199 0           foreach my $t (qw/prompt macro/) {
200 0           foreach my $d (@{ $stash->{$t} }) {
  0            
201 0           $self->_bake($d);
202             }
203             }
204             }
205              
206             # finds the path of Phrasebooks within the Library leading to Personality
207             sub _find_phrasebooks {
208 0     0     my $self = shift;
209 0 0         my @libs = (ref $self->library ? @{$self->library} : ($self->library));
  0            
210 0 0         my @alib = (ref $self->add_library ? @{$self->add_library} : ($self->add_library));
  0            
211              
212             # first find the (relative) path for the requested personality
213             # then within each of @libs gather the files along that path
214              
215 0           my $target = $self->_find_personality_in( @libs, @alib );
216 0 0         die (sprintf "error: unknown personality: '%s'\n",
217             $self->personality) unless $target;
218              
219 0           my @files = $self->_gather_pb_from( $target, @libs, @alib );
220 0 0         die (sprintf "error: personality '%s' contains no phrasebook files!\n",
221             $self->personality) unless scalar @files;
222              
223 0           return @files;
224             }
225              
226             sub _find_personality_in {
227 0     0     my ($self, @libs) = @_;
228 0           my $target = undef;
229              
230 0           foreach my $lib (@libs) {
231             Path::Class::Dir->new($lib)->recurse(callback => sub {
232 0 0   0     return unless $_[0]->is_dir;
233 0 0         $target = Path::Class::Dir->new($_[0])->relative($lib)
234             if $_[0]->dir_list(-1) eq $self->personality
235 0           });
236 0 0         last if defined $target;
237             }
238 0           return $target;
239             }
240              
241             sub _gather_pb_from {
242 0     0     my ($self, $target, @libs) = @_;
243 0           my @files = ();
244              
245 0 0 0       return () unless $target->isa('Path::Class::Dir') and $target->is_relative;
246              
247 0           foreach my $lib (@libs) {
248 0           my $root = Path::Class::Dir->new($lib);
249              
250 0           foreach my $part ($target->dir_list) {
251 0           $root = $root->subdir($part);
252             # $self->logger->log('phrasebook', 'debug', sprintf 'searching in [%s]', $root);
253 0 0         last if not -d $root->stringify;
254              
255             push @files,
256 0           sort {$a->basename cmp $b->basename}
257 0           grep { not $_->is_dir } $root->children(no_hidden => 1);
  0            
258             }
259             }
260 0           return @files;
261             }
262              
263             1;
264              
265             =pod
266              
267             =head1 NAME
268              
269             Net::CLI::Interact::Phrasebook - Load command phrasebooks from a Library
270              
271             =head1 DESCRIPTION
272              
273             A command phrasebook is where you store the repeatable sequences of commands
274             which can be sent to connected network devices. An example would be a command
275             to show the configuration of a device: storing this in a phrasebook (sometimes
276             known as a dictionary) saves time and effort.
277              
278             This module implements the loading and preparing of phrasebooks from an
279             on-disk file-based hierarchical library, and makes them available to the
280             application as smart objects for use in L<Net::CLI::Interact> sessions.
281             Entries in the phrasebook will be one of the following types:
282              
283             =over 4
284              
285             =item Prompt
286              
287             Named regular expressions that match the content of a single line of text in
288             the output returned from a connected device. They are a demarcation between
289             commands sent and responses returned.
290              
291             =item Macro
292              
293             Alternating sequences of command statements sent to the device, and regular
294             expressions to match the response. There are different kinds of Macro,
295             explained below.
296              
297             =back
298              
299             The named regular expressions used in Prompts and Macros are known as I<Match>
300             statements. The command statements in Macros which are sent to the device are
301             known as I<Send> statements. That is, Prompts and Macros are built from one or
302             more Match and Send statements.
303              
304             Each Send or Match statement becomes an instance of the
305             L<Net::CLI::Interact::Action> class. These are built up into Prompts and
306             Macros, which become instances of the L<Net::CLI::Interact::ActionSet> class.
307              
308             =head1 USAGE
309              
310             A phrasebook is a plain text file containing named Prompts or Macros. Each
311             file exists in a directory hierarchy, such that files "deeper" in the
312             hierarchy have their entries override the similarly named entries higher up.
313             For example:
314              
315             /dir1/file1
316             /dir1/file2
317             /dir1/dir2/file3
318              
319             Entries in C<file3> sharing a name with any entries from C<file1> or C<file2>
320             will take precedence. Those in C<file2> will also override entries in
321             C<file1>, because asciibetical sorting places the files in that order, and
322             later definitions with the same name and type override earlier ones.
323              
324             When this module is loaded, a I<personality> key is required. This locates a
325             directory on disk, and then the files in that directory and all its ancestors
326             in the hierarchy are loaded. The directories to search are specified by two
327             I<Library> options (see below). All phrasebooks matching the given
328             I<personality> are loaded, allowing a user to override or augment the default,
329             shipped phrasebooks.
330              
331             =head1 INTERFACE
332              
333             =head2 new( \%options )
334              
335             This takes the following options, and returns a loaded phrasebook object:
336              
337             =over 4
338              
339             =item C<< personality => $directory >> (required)
340              
341             The name of a directory component on disk. Any files higher in the libraries
342             hierarchy are also loaded, but entries in files contained within this
343             directory, or "closer" to it, will take precedence.
344              
345             =item C<< library => $directory | \@directories >>
346              
347             First library hierarchy, specified either as a single directory or a list of
348             directories that are searched in order. The idea is that this option be set in
349             your application code, perhaps specifying some directory of phrasebooks
350             shipped with the distribution.
351              
352             =item C<< add_library => $directory | \@directories >>
353              
354             Second library hierarchy, specified either as a single directory or a list of
355             directories that are searched in order. This parameter is for the end-user to
356             provide the location(s) of their own phrasebook(s). Any entries found via this
357             path will override those found via the first C<library> path.
358              
359             =back
360              
361             =head2 prompt( $name )
362              
363             Returns the Prompt associated to the given C<$name>, or throws an exception if
364             no such prompt can be found. The returned object is an instance of
365             L<Net::CLI::Interact::ActionSet>.
366              
367             =head2 has_prompt( $name )
368              
369             Returns true if a prompt of the given C<$name> exists in the loaded phrasebooks.
370              
371             =head2 prompt_names
372              
373             Returns a list of the names of the current loaded Prompts.
374              
375             =head2 macro( $name )
376              
377             Returns the Macro associated to the given C<$name>, or throws an exception if
378             no such macro can be found. The returned object is an instance of
379             L<Net::CLI::Interact::ActionSet>.
380              
381             =head2 has_macro( $name )
382              
383             Returns true if a macro of the given C<$name> exists in the loaded phrasebooks.
384              
385             =head2 macro_names
386              
387             Returns a list of the names of the current loaded Macros.
388              
389             =head1 PHRASEBOOK FORMAT
390              
391             =head2 Prompt
392              
393             A Prompt is a named regular expression which matches the content of a single
394             line of text. Here is an example:
395              
396             prompt configure
397             match /\(config[^)]*\)# ?$/
398              
399             On the first line is the keyword C<prompt> followed by the name of the Prompt,
400             which must be a valid Perl identifier (letters, numbers, underscores only).
401              
402             On the immediately following line is the keyword C<match> followed by a
403             regular expression, enclosed in two forward-slash characters. Currently, no
404             alternate bookend characters are supported, nor are regular expression
405             modifiers (such as C<xism>) outside of the match, but you can of course
406             include them within.
407              
408             The Prompt is used to find out when the connected CLI has emitted all of the
409             response to a command. Try to make the Prompt as specific as possible,
410             including line-end anchors. Remember that it will be matched against one line
411             of text, only.
412              
413             =head2 Macro
414              
415             In general, Macros are alternating sequences of commands to send to the
416             connected CLI, and regular expressions to match the end of the returned
417             response. Macros are useful for issuing commands which have intermediate
418             prompts, or confirmation steps. They also support the I<slurping> of
419             additional output when the connected CLI has split the response into pages.
420              
421             At its simplest a Macro can be just one command:
422              
423             macro show_int_br
424             send show ip int br
425             match /> ?$/
426              
427             On the first line is the keyword C<macro> followed by the name of the Macro,
428             which must be a valid Perl identifier (letters, numbers, underscores only).
429              
430             On the immediately following line is the keyword C<send> followed by a space
431             and then any text up until the end of the line, and if you want to include
432             whitespace at the beginning or end of the command, use quotes. This text is
433             sent to the connected CLI as a single command statement. The next line
434             contains the keyword C<match> followed by the Prompt (regular expression)
435             which will terminate gathering of returned output from the sent command.
436              
437             Macros support the following features:
438              
439             =over 4
440              
441             =item Automatic Matching
442              
443             Normally, you ought always to specify C<send> statements along with a
444             following C<match> statement so that the module can tell when the output from
445             your command has ended. However you can omit any Match and the module will
446             insert either the current C<prompt> value if set by the user, or the last
447             Prompt from the last Macro. So the previous example could be re-written as:
448              
449             macro show_int_br
450             send show ip int br
451              
452             You can have as many C<send> statements as you like, and the Match statements
453             will be inserted for you:
454              
455             macro show_int_br_and_timestamp
456             send show ip int br
457             send show clock
458              
459             However it is recommended that this type of sequence be implemented as
460             individual commands (or separate Macros) rather than a single Macro, as it
461             will be easier for you to retrieve the command response(s). Normally the
462             Automatic Matching is used just to allow missing off of the final Match
463             statement when it's the same as the current Prompt.
464              
465             =item Format Interpolation
466              
467             Each C<send> statement is in fact run through Perl's C<sprintf> command, so
468             variables may be interpolated into the statement using standard C<"%"> fields.
469             For example:
470              
471             macro show_int_x
472             send show interface %s
473              
474             The method for passing variables into the module upon execution of this Macro
475             is documented in L<Net::CLI::Interact::Role::Engine>. This feature is useful
476             for username/password prompts.
477              
478             =item Named Match References
479              
480             If you're going to use the same Match (regular expression) in a number of
481             Macros, then set it up as a Prompt (see above) and refer to it by name,
482             instead:
483              
484             prompt priv_exec
485             match /# ?$/
486            
487             macro to_priv_exec
488             send enable
489             match /[Pp]assword: ?$/
490             send %s
491             match priv_exec
492              
493             As you can see, in the case of the last Match, we have the keyword C<match>
494             followed by the name of a defined Prompt. To match multiple defined Prompts
495             use this syntax (with as many named references as you like):
496              
497             macro to_privileged
498             send enable
499             match username_prompt or priv_exec
500              
501             =item Continuations
502              
503             Sometimes the connected CLI will not know it's talking to a program and so
504             paginate the output (that is, split it into pages). There is usually a
505             keypress required between each page. This is supported via the following
506             syntax:
507              
508             macro show_run
509             send show running-config
510             follow / --More-- / with ' '
511              
512             On the line following the C<send> statement is the keyword C<follow> and a
513             regular expression enclosed in forward-slashes. This is the Match which will,
514             if seen in the command output, trigger the continuation. On the line you then
515             have the keyword C<with> followed by a space and some text, until the end of
516             the line. If you need to enclose whitespace use quotes, as in the example.
517              
518             The module will send the continuation text and gobble the matched prompt from
519             the emitted output so you only have one complete piece of text returned, even
520             if split over many pages. The sent text can contain metacharacters such as
521             C<\n> for a newline.
522              
523             Note that in the above example the C<follow> statement should be seen as an
524             extension of the C<send> statement. There is still an implicit Match prompt
525             added at the end of this Macro, as per Automatic Matching, above.
526              
527             =item Line Endings
528              
529             Normally all sent command statements are appended with a newline (or the value
530             of C<ors>, if set). To suppress that feature, use the keyword C<put> instead
531             of C<send>. However this does not prevent the Format Interpolation via
532             C<sprintf> as described above (simply use C<"%%"> to get a literal C<"%">).
533              
534             =back
535              
536             =cut
537