File Coverage

blib/lib/Config/Augeas/Exporter.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             # Copyright (c) 2011 RaphaĆ«l Pinson.
2             #
3             # This library is free software; you can redistribute it and/or
4             # modify it under the terms of the GNU Lesser Public License as
5             # published by the Free Software Foundation; either version 2.1 of
6             # the License, or (at your option) any later version.
7             #
8             # Config-Model is distributed in the hope that it will be useful,
9             # but WITHOUT ANY WARRANTY; without even the implied warranty of
10             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11             # Lesser Public License for more details.
12             #
13             # You should have received a copy of the GNU Lesser Public License
14             # along with Config-Model; if not, write to the Free Software
15             # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
16             # 02110-1301 USA
17              
18             package Config::Augeas::Exporter;
19 2     2   29752 use strict;
  2         6  
  2         82  
20 2     2   12 use warnings;
  2         4  
  2         74  
21 2     2   9 use base qw(Class::Accessor);
  2         13  
  2         2076  
22              
23 2     2   8123 use Config::Augeas qw(get match count_match);
  0            
  0            
24             use XML::LibXML;
25             use Encode qw(encode);
26             use YAML qw(Dump);
27             use JSON qw();
28             use File::Path qw(mkpath);
29              
30             __PACKAGE__->mk_accessors(qw(to_xml to_hash to_yaml to_json from_xml));
31              
32             our $VERSION = '1.0.0';
33              
34             # Default values
35             my $PATH = '/files';
36              
37              
38             =head1 NAME
39              
40             Config::Augeas::Exporter - Export the Augeas tree to various formats
41              
42             =head1 SYNOPSIS
43              
44             use Config::Augeas::Exporter
45              
46             # Initiliaze
47             my $aug = Config::Augeas::Exporter->new( root => $aug_root );
48              
49             # Export to XML
50             my $doc = $aug->to_xml(
51             path => ['/files/etc/fstab', '/files/etc/apt'],
52             exclude => ['#comment', '#mcomment'],
53             file_stat => 1,
54             );
55              
56             print $doc->toString;
57              
58             # Restore from XML
59             open (my $fh, "<$file")
60             or die "E: Could not open $file: $!\n" ;
61             my $doc = XML::LibXML->load_xml(IO => $fh);
62             close $fh;
63              
64             my $aug = Config::Augeas::Exporter->new(root => $root);
65             $aug->from_xml(
66             xml => $doc,
67             create_dirs => 1,
68             );
69              
70              
71             =head1 DESCRIPTION
72              
73             This module allows to export the Augeas tree to various formats and import back from these formats to the configuration files.
74              
75             =head1 Constructor
76              
77             =head1 new ( ... )
78              
79             Creates a new Config::Augeas::Exporter object. Optional parameters are:
80              
81             =over
82              
83             =item augeas
84              
85             A Config::Augeas object. If not provided, a new one will be created.
86              
87             =item root
88              
89             Use C as the filesystem root.
90              
91             =back
92              
93             =cut
94              
95              
96             sub new {
97             my $class = shift;
98             my %options = @_;
99              
100             my $root = $options{root};
101             $root ||= '';
102              
103             $class = ref $class || $class || __PACKAGE__;
104             my $self = __PACKAGE__->SUPER::new();
105              
106             # Initiliaze Augeas if it wasn't passed
107             my $aug = $options{augeas};
108             unless($aug) {
109             $aug = Config::Augeas->new(root => $root);
110             }
111              
112             # Associate to object
113             $self->{aug} = $aug;
114              
115             # Get augeas root
116             $self->{aug_root} = $aug->get('/augeas/root');
117              
118             return $self;
119             }
120              
121              
122             =head1 Methods
123              
124             =head2 to_xml( ... )
125              
126             Export the Augeas tree to a XML::LibXML::Document object.
127              
128             =over
129              
130             =item path
131              
132             An array of Augeas paths to export. If ommitted, it will default to '/files'.
133              
134             =item exclude
135              
136             An array of label patterns to exclude from the export.
137              
138             =item file_stat
139              
140             A boolean, whether to include file stat.
141              
142             =back
143              
144             =cut
145              
146              
147             sub to_xml {
148             my $self = shift;
149             my %args = @_;
150              
151             my @paths = @{$args{path}} if $args{path};
152             my @excludes = @{$args{exclude}} if $args{exclude};
153             my $file_stat = $args{file_stat};
154              
155             # Defaults
156             @paths = $PATH if ($#paths < 0);
157              
158             # Initialize XML document
159             my $doc = XML::LibXML::Document->new('1.0', 'utf-8');
160              
161             # Get XML elements from augeas recursively
162             my @file_elems;
163             for my $path (@paths) {
164             my @new_file_elems = (node_to_xml($self, $path, \@excludes, 1, $file_stat));
165             map { push @file_elems, $_ } @new_file_elems;
166             }
167              
168             # Add a files node for all file entries
169             my $files = XML::LibXML::Element->new('files');
170             map { $files->appendChild($_) } @file_elems;
171              
172             # Raise warning if no "file" node was found
173             my @node_nodes = $files->findnodes("//node");
174             my @file_nodes = $files->findnodes("//file");
175              
176             if ($#node_nodes >= 0 && $#file_nodes < 0) {
177             warn "W: The XML export contains no file nodes.
178             W: You will not be able to import it back.\n";
179             }
180              
181             # Add an error node for errors
182             my @error_elems = (node_to_xml($self, '/augeas//error', [], 0, 0));
183             my $errors = XML::LibXML::Element->new('error');
184             map { $errors->appendChild($_) } @error_elems;
185              
186             # Add an augeas node on top
187             my $augeas = XML::LibXML::Element->new('augeas');
188             $augeas->appendChild($files);
189             $augeas->appendChild($errors);
190              
191             # Associate files node with document
192             $doc->setDocumentElement($augeas);
193              
194             return $doc;
195             }
196              
197              
198             sub node_to_xml {
199             my ($self, $path, $excludes, $check_is_file, $incl_file_stat) = @_;
200              
201             # Default check is_file
202             $check_is_file = 1 unless(defined($check_is_file));
203              
204             # Default incl_file_stat
205             $incl_file_stat = 0 unless(defined($incl_file_stat));
206              
207             # Get label from path
208             my $label = get_label($path);
209              
210             # Filter excludes
211             return if exclude_match($label, $excludes);
212              
213             # Sanitize path for augeas requests
214             $path = sanitize_path($path);
215              
216             my $aug = $self->{aug};
217             my @children = $aug->match("$path/*");
218              
219             # Should children check is_file?
220             my $children_check_is_file = $check_is_file;
221             if ($check_is_file && is_file($self, $path)) {
222             $children_check_is_file = 0;
223             }
224              
225             # Parse children
226             my @child_elems;
227             for my $child (@children) {
228             my @new_child_elems = (node_to_xml($self, $child, $excludes, $children_check_is_file, $incl_file_stat));
229             map { push @child_elems, $_ } @new_child_elems;
230             }
231              
232             # Directories don't get their own node
233             return @child_elems if ($check_is_file && $self->is_dir($path));
234              
235             # Files and entries get their own nodes
236             my $elem;
237             if ($check_is_file && is_file($self, $path)) {
238             # Files get nodes
239             $elem = XML::LibXML::Element->new('file');
240             my $file_path = get_file_path($path);
241             $elem->setAttribute("path", $file_path);
242            
243             # Include file stat if requested
244             if ($incl_file_stat) {
245             my $file_stat = $self->stat_to_xml($file_path);
246             $elem->appendChild($file_stat);
247             }
248             } else {
249             # Entries get nodes
250             $elem = XML::LibXML::Element->new('node');
251             $elem->setAttribute("label", $label);
252             }
253              
254             # Append children to element
255             map { $elem->appendChild($_) } @child_elems;
256              
257             # Add value to element
258             my $value = $aug->get($path);
259             if (defined($value)) {
260             my $value_elem = XML::LibXML::Element->new('value');
261             $value_elem->appendTextNode(encode('utf-8', $value)) if defined($value);
262             $elem->appendChild($value_elem);
263             }
264              
265             return $elem;
266             }
267              
268              
269             =head2 to_hash( ... )
270              
271             Export the Augeas tree to a hash.
272              
273             =over
274              
275             =item path
276              
277             C is the Augeas path to export. If ommitted, it will default to '/files'.
278              
279             =item exclude
280              
281             A list of label patterns to exclude from the export.
282              
283             =back
284              
285             =cut
286              
287              
288             sub to_hash {
289             my $self = shift;
290             my %args = @_;
291              
292             my $path = $args{path};
293             my @excludes = ($args{exclude});
294              
295             # Default path
296             $path ||= $PATH;
297              
298             my @file_elems = (node_to_hash($self, $path, \@excludes));
299              
300             my %hash = (
301             files => \@file_elems,
302             );
303              
304             return \%hash;
305             }
306              
307              
308             sub node_to_hash {
309             my ($self, $path, $excludes) = @_;
310              
311             # Get label from path
312             my $label = get_label($path);
313              
314             # Filter excludes
315             return if exclude_match($label, $excludes);
316              
317             # Sanitize path for augeas requests
318             $path = sanitize_path($path);
319              
320             my $aug = $self->{aug};
321             my @children = $aug->match("$path/*");
322              
323             # Parse children
324             my @child_elems;
325             for my $child (@children) {
326             my @new_child_elems = (node_to_hash($self, $child, $excludes));
327             map { push @child_elems, $_ } @new_child_elems;
328             }
329              
330             # Directories don't get their own node
331             return @child_elems if ($self->is_dir($path));
332              
333             # Initialize array
334             my %hash;
335             my $node_hash;
336              
337             if (is_file($self, $path)) {
338             my $file_path = get_file_path($path);
339             $node_hash = \%{$hash{$file_path}};
340             } else {
341             $node_hash = \%{$hash{$label}};
342             }
343              
344             # Append children
345             map { push @{$node_hash->{children}}, $_ } @child_elems;
346              
347             # Add value to element
348             my $value = $aug->get($path);
349             $node_hash->{value} = $value if defined($value);
350              
351             return \%hash;
352             }
353              
354              
355             =head2 to_yaml( ... )
356              
357             Export the Augeas tree to YAML.
358              
359             =over
360              
361             =item path
362              
363             C is the Augeas path to export. If ommitted, it will default to '/files'.
364              
365             =item exclude
366              
367             A list of label patterns to exclude from the export.
368              
369             =back
370              
371             =cut
372              
373             sub to_yaml {
374             my $self = shift;
375             my %args = @_;
376              
377             my $hash = $self->to_hash(%args);
378              
379             return YAML::Dump($hash);
380             }
381              
382              
383             =head2 to_json( ... )
384              
385             Export the Augeas tree to JSON.
386              
387             =over
388              
389             =item path
390              
391             C is the Augeas path to export. If ommitted, it will default to '/files'.
392              
393             =item exclude
394              
395             A list of label patterns to exclude from the export.
396              
397             =back
398              
399             =cut
400              
401             sub to_json {
402             my $self = shift;
403             my %args = @_;
404              
405             my $hash = $self->to_hash(%args);
406              
407             my $json = new JSON;
408             return $json->encode($hash);
409             }
410              
411              
412             =head2 from_xml( ... )
413              
414             Restore the Augeas tree from an XML::LibXML::Document object.
415             This method considers the files listed in the XML document,
416             and replaces the corresponding files in the Augeas tree with
417             the contents of the XML.
418              
419             =over
420              
421             =item xml
422              
423             The XML::LibXML::Document to use as source for import.
424              
425             =item create_dirs
426              
427             Boolean value, whether to create the directories if missing.
428              
429             =back
430              
431             =cut
432              
433              
434             sub from_xml {
435             my $self = shift;
436             my %args = @_;
437              
438             die "E: No XML provided." unless(defined($args{xml}));
439              
440             my $xml = $args{xml};
441             my $create_dirs = $args{create_dirs};
442             my $aug = $self->{aug};
443              
444             my @files = $xml->find('/augeas/files/file')->get_nodelist();
445              
446             if ($#files < 0) {
447             warn "W: The XML document contains no file to restore.";
448             return;
449             }
450              
451             # Get augeas root to create directories
452             my $aug_root = $self->{aug_root};
453             die "E: Could not determine Augeas root needed to create directories."
454             if ($create_dirs && !defined($aug_root));
455              
456             # Add each file to the Augeas tree
457             for my $file (@files) {
458             my $path = $file->getAttribute('path');
459              
460             # Create directories if requested
461             if($create_dirs) {
462             if ($path =~ m|^(.*)/([^/]+)$|) {
463             my $dir = "${aug_root}${1}";
464             unless (-d $dir) {
465             mkpath($dir) or die "E: Failed to create directory $dir: $!";
466             }
467             } else {
468             die "E: Could not get directory from file path $path.";
469             }
470             }
471              
472             my $aug_path = "/files${path}";
473             # Clean the Augeas tree for this file
474             $aug->rm($aug_path);
475              
476             for my $node ($file->childNodes) {
477             $self->xml_to_node($node, $aug_path);
478             }
479             }
480              
481             $aug->save;
482             $aug->print('/augeas//error');
483             }
484              
485              
486             sub xml_to_node {
487             my ($self, $elem, $path) = @_;
488              
489             my $aug = $self->{aug};
490              
491             my $name = $elem->nodeName;
492             my $label = $elem->getAttribute('label');
493              
494             # Ignore stat nodes
495             return if ($name eq 'stat');
496              
497             my $matchpath = "$path/*[last()]";
498             $matchpath = sanitize_path($matchpath);
499             my $lastpath = sanitize_path($aug->match("$path/*[last()]"));
500              
501             if(defined($lastpath)) {
502             # Insert last node
503             $aug->insert($label, "after", $lastpath);
504             } else {
505             # Config::Augeas doesn't take undef
506             # as a correct value or provide clear
507             # This is an ugly trick to do the same
508             # hoping the previous children did not
509             # create a ##foo node
510             my $create_path = sanitize_path("$path/$label/#foo");
511             $aug->set($create_path, "foo");
512             $aug->rm($create_path);
513             }
514              
515             $matchpath = sanitize_path("${path}/${label}[last()]");
516             my $newpath = sanitize_path($aug->match($matchpath));
517              
518             my $value;
519              
520             for my $child ($elem->childNodes()) {
521             if ($child->nodeName eq 'value') {
522             # Text node
523             $value = $child->textContent;
524             } else {
525             $self->xml_to_node($child, $newpath);
526             }
527             }
528              
529             if (defined($value)) {
530             $aug->set($newpath, $value);
531             }
532             }
533              
534              
535             ##############
536             # Useful subs
537             ##############
538              
539             sub get_file_path {
540             my ($path) = @_;
541              
542             my $file_path = $path;
543             $file_path =~ s|^/files||;
544            
545             return $file_path;
546             }
547              
548              
549             sub get_label {
550             my ($path) = @_;
551              
552             # Get label from path
553             my $label = '';
554              
555             if ($path =~ m|.*/([^/\[]+)(\[\d+\])?|) {
556             $label = $1;
557             } else {
558             die "E: Could not parse $path\n";
559             }
560              
561             return $label;
562             }
563              
564              
565             sub exclude_match {
566             my ($label, $excludes) = @_;
567              
568             # Filter excludes
569             for my $exclude (@$excludes) {
570             if ($exclude && $label =~ /$exclude/) {
571             return 1;
572             }
573             }
574              
575             return 0;
576             }
577              
578              
579             sub sanitize_path {
580             my ($path) = @_;
581              
582             return unless ($path);
583              
584             # Sanitize path for augeas requests
585             $path =~ s|(?<=[^\\]) |\\ |g;
586              
587             return $path;
588             }
589              
590              
591             sub is_file {
592             my ($self, $path) = @_;
593              
594             my $aug_path = "/augeas${path}/path";
595             my $aug = $self->{aug};
596             my $count = $aug->count_match($aug_path);
597              
598             # Not a file if there is no path subnode
599             return 0 if ($count == 0);
600             # Check that the subnode has the right value
601             return 1 if ($aug->get($aug_path) eq $path);
602             # Otherwise it's not a file
603             return 0;
604             }
605              
606              
607             sub is_dir {
608             my ($self, $path) = @_;
609              
610             my $aug_path = "/augeas${path}";
611             my $aug = $self->{aug};
612             my $count = $aug->count_match($aug_path);
613             my $value = $aug->get($aug_path);
614              
615             # A directory is not a file
616             # but its path must exist in /augeas
617             # and have no value associated to it
618             return 1 if (!$self->is_file($path) && $count == 1
619             && !defined($value));
620             # Otherwise it's not a directory
621             return 0;
622             }
623              
624              
625             sub stat_to_xml {
626             my ($self, $path) = @_;
627             my $aug_root = $self->{aug_root};
628              
629             my %stat;
630             ($stat{dev}, $stat{ino}, $stat{mode}, $stat{nlink},
631             $stat{uid}, $stat{gid}, $stat{rdev}, $stat{size},
632             $stat{atime}, $stat{mtime}, $stat{ctime},
633             $stat{blksize}, $stat{blocks}) = stat("${aug_root}${path}");
634              
635             my $stat_elem = XML::LibXML::Element->new('stat');
636              
637             for my $k (keys(%stat)) {
638             $stat_elem->setAttribute($k, $stat{$k});
639             }
640              
641             return $stat_elem;
642             }
643              
644              
645             __END__