File Coverage

blib/lib/VM/Libvirt/CloneHelper.pm
Criterion Covered Total %
statement 14 181 7.7
branch 0 76 0.0
condition n/a
subroutine 5 16 31.2
pod 11 11 100.0
total 30 284 10.5


line stmt bran cond sub pod time code
1             package VM::Libvirt::CloneHelper;
2              
3 1     1   99797 use 5.006;
  1         5  
4 1     1   6 use strict;
  1         2  
  1         49  
5 1     1   7 use warnings;
  1         2  
  1         67  
6 1     1   670 use File::Slurp qw(write_file read_file);
  1         49638  
  1         86  
7 1     1   9 use File::Temp;
  1         5  
  1         3642  
8              
9             =head1 NAME
10              
11             VM::Libvirt::CloneHelper - Create a bunch of cloned VMs in via libvirt.
12              
13             =head1 VERSION
14              
15             Version 0.1.1
16              
17             =cut
18              
19             our $VERSION = '0.1.1';
20              
21             =head1 SYNOPSIS
22              
23             # initialize it
24             my $clone_helper=VM::Libvirt::CloneHelper->new({
25             blank_domains=>'/usr/local/etc/clonehelper/blank_domains',
26             net_head=>'/usr/local/etc/clonehelper/net_head',
27             net_tail=>'/usr/local/etc/clonehelper/net_tail',
28             windows_blank=>0,
29             mac_base=>'00:08:74:2d:dd:',
30             ipv4_base=>'192.168.1.',
31             start=>100,
32             to_clone=>'baseVM',
33             clone_name_base=>'foo',
34             count=>10,
35             verbose=>1,
36             snapshot_name=>'clean',
37             net=>'default',
38             wait=>360,
39             });
40              
41             $clone_helper->delete_vms;
42             $clone_helper->clone_vms;
43             $clone_helper->start_vms;
44             sleep 500;
45             $clone_helper->snapshot_vms;
46             $clone_helper->shutdown_vms;
47              
48             It should be noted that this is effectively limited to 253 VMs.
49              
50             This script lib is primarily meant for creating a bunch of cloned VMs on a
51             box for testing purposes, so this is not really a major issue given the
52             design scope.
53              
54             VMs should be set to us DHCP so they will get their expected IP when they boot.
55              
56             =head1 METHODS
57              
58             =head2 new
59              
60             Initialize the module.
61              
62             net=>'default'
63             Name of the libvirt network in question.
64              
65             blank_domains=>'/usr/local/etc/clonehelper/blank_domains',
66             List of domains to blank via setting 'dnsmasq:option value='address=/foo.bar/'.
67             If not this file does not exist, it will be skipped.
68              
69             net_head=>'/usr/local/etc/clonehelper/net_head',
70             The top part of the net XML config that that dnsmasq options will be
71             sandwhiched between.
72              
73             net_tail=>'/usr/local/etc/clonehelper/net_tail',
74             The bottom part of the net XML config that that dnsmasq options will
75             be sandwhiched between.
76              
77             windows_blank=>1,
78             Blank commonly used MS domains. This is handy for reducing network noise
79             when testing as well as making sure they any VMs don't do something like
80             run updates when one does not want it to.
81              
82             mac_base=>'00:08:74:2d:dd:',
83             Base to use for the MAC.
84              
85             ipv4_base=>'192.168.1.',
86             Base to use for the IPs for adding static assignments.
87              
88             start=>'100',
89             Where to start in set.
90              
91             to_clone=>'baseVM',
92             The name of the VM to clone.
93              
94             clone_name_base=>'cloneVM',
95             Base name to use for creating the clones. 'foo' will become 'foo$current', so
96             for a start of 100, the first one would be 'foo100' and with a count of 10 the
97             last will be 'foo109'.
98              
99             count=>10,
100             How many clones to create.
101              
102             snapshot_name=>'clean',
103             The name to use for the snapshot.
104              
105             wait=>360,
106             How long to wait if auto-doing all.
107              
108             =cut
109              
110             sub new {
111 0     0 1   my %args;
112 0 0         if ( defined( $_[1] ) ) {
113 0           %args = %{ $_[1] };
  0            
114             }
115              
116 0           my $self = {
117             blank_domains => '/usr/local/etc/clonehelper/blank_domains',
118             net_head => '/usr/local/etc/clonehelper/net_head',
119             net_tail => '/usr/local/etc/clonehelper/net_tail',
120             windows_blank => 1,
121             mac_base => '00:08:74:2d:dd:',
122             ipv4_base => '192.168.1.',
123             start => 100,
124             to_clone => 'baseVM',
125             delete_old => 1,
126             clone_name_base => 'foo',
127             uuid_auto => 1,
128             count => 10,
129             verbose => 1,
130             snapshot_name => 'clean',
131             net => 'default',
132             wait => 360,
133             };
134 0           bless $self;
135              
136             # do very basic value sanity checks and reel values in
137 0           my @keys = keys(%args);
138 0           foreach my $key (@keys) {
139 0 0         if ( $key eq 'mac_base' ) {
    0          
    0          
    0          
140              
141             # make sure we got a sane base MAC
142 0 0         if ( $args{mac_base}
143             !~ /^[0-9aAbBcCdDeEfF][0-9aAbBcCdDeEfF]\:[0-9aAbBcCdDeEfF][0-9aAbBcCdDeEfF]\:[0-9aAbBcCdDeEfF][0-9aAbBcCdDeEfF]\:[0-9aAbBcCdDeEfF][0-9aAbBcCdDeEfF]\:[0-9aAbBcCdDeEfF][0-9aAbBcCdDeEfF]\:$/
144             )
145             {
146 0           die( '"' . $args{mac_base} . '" does not appear to be a valid base for a MAC address' );
147             }
148             } elsif ( $key eq 'ipv4_base' ) {
149              
150             # make sure we have a likely sane base for the IPv4 address
151 0 0         if ( $args{ipv4_base} !~ /^[0-9]+\.[0-9]+\.[0-9]+\.$/ ) {
152 0           die( '"' . $args{ipv4_base} . '" does not appear to be a valid base for a IPv4 address' );
153             }
154             } elsif ( $key eq 'to_clone' ) {
155              
156             # make sure we have a likely sane base VM name
157 0 0         if ( $args{to_clone} !~ /^[A-Za-z0-9\-\.]+$/ ) {
158 0           die( '"' . $args{to_clone} . '" does not appear to be a valid VM name' );
159             }
160             } elsif ( $key eq 'clone_name_base' ) {
161              
162             # make sure we have a likely sane base name to use for creating clones
163 0 0         if ( $args{clone_name_base} !~ /^[A-Za-z0-9\-\.]+$/ ) {
164 0           die( '"' . $args{clone_name_base} . '" does not appear to be a valid VM name' );
165             }
166             }
167              
168             # likely good, adding
169 0           $self->{$key} = $args{$key};
170             } ## end foreach my $key (@keys)
171              
172 0           $self->{end} = $self->{start} + $self->{count} - 1;
173              
174 0           $self->{VMs} = $self->vm_list;
175              
176 0           return $self;
177             } ## end sub new
178              
179             =head2 clone
180              
181             Create the clones.
182              
183             One optional argument is taken and that is the VM to operate on.
184             Otherwise all is ran for them all.
185              
186             $clone_helper->clone;
187              
188             =cut
189              
190             sub clone {
191 0     0 1   my $self = $_[0];
192 0           my $name = $_[1];
193              
194 0           my $VMs = $self->vm_list;
195              
196 0           my @VM_names;
197 0 0         if ( defined($name) ) {
198 0 0         if ( !defined( $VMs->{$name} ) ) {
199 0           die( '"' . $VMs . '" is not a known VM' );
200             }
201 0           push( @VM_names, $name );
202             } else {
203 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
204             }
205 0           foreach my $name (@VM_names) {
206             print "Cloning '"
207             . $self->{to_clone}
208             . "' to '"
209             . $name . "'("
210             . $VMs->{$name}{mac} . ", "
211             . $VMs->{$name}{ip}
212 0           . ")...\n";
213              
214 0           my @args = ( 'virt-clone', '-m', $VMs->{$name}{mac}, '-o', $self->{to_clone}, '--auto-clone', '-n', $name );
215 0 0         system(@args) == 0 or die("system '@args' failed... $?");
216             } ## end foreach my $name (@VM_names)
217             } ## end sub clone
218              
219             =head2 delete_clones
220              
221             Delete all the clones
222              
223             One optional argument is taken and that is the VM to operate on.
224             Otherwise all is ran for them all.
225              
226             $clone_helper->delete_clones;
227              
228             =cut
229              
230             sub delete_clones {
231 0     0 1   my $self = $_[0];
232 0           my $name = $_[1];
233              
234             # virsh undefine --snapshots-metadata
235             # the VM under /var/lib/libvirt/images needs to be removed manually given
236             # the shit show that is libvirt does not have a means of sanely removing
237             # VMs and relevant storage... for example it will include ISOs in relevant
238             # VMs to be removed if you let it... and it is likely to fail to remove the
239             # base disk image for a VM, even if you pass it any/every combination of
240             # possible flags...
241              
242 0           my $VMs = $self->vm_list;
243              
244 0           my @VM_names;
245 0 0         if ( defined($name) ) {
246 0 0         if ( !defined( $VMs->{$name} ) ) {
247 0           die( '"' . $VMs . '" is not a known VM' );
248             }
249 0           push( @VM_names, $name );
250             } else {
251 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
252             }
253 0           foreach my $name (@VM_names) {
254 0           print "Undefining " . $name . "\n";
255 0           my @args = ( 'virsh', 'undefine', '--snapshots-metadata', $name );
256 0 0         system(@args) == 0 or warn("system '@args' failed... $?");
257              
258 0           my $image = '/var/lib/libvirt/images/' . $name . '.qcow2';
259              
260 0 0         if ( -f $image ) {
261 0           print "Unlinking " . $image . "\n";
262 0 0         unlink($image) or die( 'unlinking "' . $image . '" failed... ' . $! );
263             }
264             } ## end foreach my $name (@VM_names)
265             } ## end sub delete_clones
266              
267             =head2 net_xml
268              
269             Returns a string with the full net config XML.
270              
271             my $net_config_xml=$clone_helper->net_xml;
272             print $net_config_xml;
273              
274             =cut
275              
276             sub net_xml {
277 0     0 1   my $self = $_[0];
278              
279 0           my $VMs = $self->vm_list;
280              
281 0 0         my $xml = read_file( $self->{net_head} ) or die( 'Failed to read "' . $self->{net_head} . '"' );
282 0 0         my $xml_tail = read_file( $self->{net_tail} ) or die( 'Failed to read "' . $self->{net_tail} . '"' );
283              
284 0 0         if ( $self->{windows_blank} ) {
285 0           $xml = $xml . '
286            
287            
288            
289            
290            
291            
292            
293            
294            
295            
296            
297            
298            
299            
300            
301            
302            
303            
304            
305            
306            
307            
308            
309            
310            
311             ';
312             } ## end if ( $self->{windows_blank} )
313              
314 0 0         if ( -f $self->{blank_domains} ) {
315 0 0         my $blank_raw = read_file( $self->{blank_domains} ) or die( 'Failed to read "' . $self->{blank_domains} . '"' );
316              
317             # remove any blank lines or anyhting commented out
318 0           my @blank_split = grep( !/^[\ \t]*]$/, grep( !/^[\ \t]*#/, split( /\n/, $blank_raw ) ) );
319 0           foreach my $line (@blank_split) {
320 0           chomp($line);
321 0           $line =~ s/^[\ \t]*//;
322 0           $line =~ s/[\ \t]*$//;
323 0           foreach my $domain ( split( /[\ \t]+/, $line ) ) {
324 0           $xml = $xml . " \n";
325             }
326             }
327             } ## end if ( -f $self->{blank_domains} )
328              
329 0           my @VM_names = sort( keys( %{$VMs} ) );
  0            
330 0           foreach my $name (@VM_names) {
331             $xml
332             = $xml
333             . '
334             . $VMs->{$name}{mac} . ','
335 0           . $VMs->{$name}{ip} . '\'/>' . "\n";
336             }
337              
338 0           return $xml . $xml_tail;
339             } ## end sub net_xml
340              
341             =head2 net_redefine
342              
343             Redefines the network in question.
344              
345             =cut
346              
347             sub net_redefine {
348 0     0 1   my $self = $_[0];
349              
350 0           my $xml = $self->net_xml;
351              
352 0           print "Undefining the the network('" . $self->{net} . "') for readding it...\n";
353 0           my @args = ( 'virsh', 'net-undefine', $self->{net} );
354 0 0         system(@args) == 0 or die("system '@args' failed... $?");
355              
356 0           my $fh = File::Temp->new;
357 0           my $tmp_file = $fh->filename;
358              
359 0 0         write_file( $tmp_file, $xml ) or die( 'Failed to write tmp net config to "' . $tmp_file . '"... ' . $@ );
360              
361 0           print "Defining the the network('" . $self->{net} . "') for readding it...\n";
362 0           @args = ( 'virsh', 'net-define', '--file', $tmp_file );
363 0 0         system(@args) == 0 or die("system '@args' failed... $?");
364              
365 0 0         unlink($tmp_file) or die( 'Failed to unlink net config "' . $tmp_file . '"... ' . $@ );
366              
367 0           return;
368             } ## end sub net_redefine
369              
370             =head2 recreate
371              
372             Recreate the specified VM.
373              
374             One optional argument is taken and that is the VM to operate on.
375             Otherwise all is ran for them all.
376              
377             If you wish to recreate all, you should likely use recreate_all, to avoid
378             any issues caused by starting them all at the same time.
379              
380             $clone_helper->recreate('foo100');
381              
382             =cut
383              
384             sub recreate {
385 0     0 1   my $self = $_[0];
386 0           my $name = $_[0];
387              
388 0 0         if ( !defined($name) ) {
389 0           die('No VM specified to recreate');
390             }
391              
392 0           my $VMs = $self->vm_list;
393              
394 0 0         if ( !defined( $VMs->{$name} ) ) {
395 0           die( '"' . $VMs . '" is not a known VM' );
396             }
397              
398 0           $self->delete_clones($name);
399 0           $self->clone($name);
400 0           $self->start_clones($name);
401 0           sleep( $self->{wait} );
402 0           $self->snapshot_clones($name);
403 0           $self->stop_clones($name);
404              
405 0           return;
406             } ## end sub recreate
407              
408             =head2 recreate_all
409              
410             Recreate all VMs.
411              
412             Does one at a time.
413              
414             $clone_helper->recreate_all;
415              
416             =cut
417              
418             sub recreate_all {
419 0     0 1   my $self = $_[0];
420              
421 0           my $VMs = $self->vm_list;
422              
423 0           my @VM_names = sort( keys( %{$VMs} ) );
  0            
424 0           foreach my $name (@VM_names) {
425 0           $self->delete_clones($name);
426 0           $self->clone($name);
427 0           $self->start_clones($name);
428 0           sleep( $self->{wait} );
429 0           $self->snapshot_clones($name);
430 0           $self->stop_clones($name);
431             }
432              
433 0           return;
434             } ## end sub recreate_all
435              
436             =head2 snapshot_clones
437              
438             Snapshot all the clones
439              
440             One optional argument is taken and that is the VM to operate on.
441             Otherwise all is ran for them all.
442              
443             $clone_helper->snapshot_clones;
444              
445             =cut
446              
447             sub snapshot_clones {
448 0     0 1   my $self = $_[0];
449 0           my $name = $_[1];
450              
451 0           my $VMs = $self->vm_list;
452              
453 0           my @VM_names;
454 0 0         if ( defined($name) ) {
455 0 0         if ( !defined( $VMs->{$name} ) ) {
456 0           die( '"' . $VMs . '" is not a known VM' );
457             }
458 0           push( @VM_names, $name );
459             } else {
460 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
461             }
462 0           foreach my $name (@VM_names) {
463 0           print "Snapshotting " . $name . "...\n";
464 0           my @args = ( 'virsh', 'snapshot-create-as', '--name', $self->{snapshot_name}, $name );
465 0 0         system(@args) == 0 or die("system '@args' failed... $?");
466             }
467             } ## end sub snapshot_clones
468              
469             =head2 start_clones
470              
471             Start all the clones
472              
473             One optional argument is taken and that is the VM to operate on.
474             Otherwise all is ran for them all.
475              
476             $clone_helper->start_clones;
477              
478             =cut
479              
480             sub start_clones {
481 0     0 1   my $self = $_[0];
482 0           my $name = $_[1];
483              
484 0           my $VMs = $self->vm_list;
485              
486 0           my @VM_names;
487 0 0         if ( defined($name) ) {
488 0 0         if ( !defined( $VMs->{$name} ) ) {
489 0           die( '"' . $VMs . '" is not a known VM' );
490             }
491 0           push( @VM_names, $name );
492             } else {
493 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
494             }
495 0           foreach my $name (@VM_names) {
496 0           print "Starting " . $name . "...\n";
497 0           my @args = ( 'virsh', 'start', $name );
498 0 0         system(@args) == 0 or die("system '@args' failed... $?");
499             }
500             } ## end sub start_clones
501              
502             =head2 stop_clones
503              
504             Stop all the clones. This does not stop them gracefully as we don't
505             need to as they are being started via snapshot.
506              
507             One optional argument is taken and that is the VM to operate on.
508             Otherwise all is ran for them all.
509              
510             $clone_helper->stop_clones;
511              
512             =cut
513              
514             sub stop_clones {
515 0     0 1   my $self = $_[0];
516 0           my $name = $_[1];
517              
518 0           my $VMs = $self->vm_list;
519              
520 0           my @VM_names;
521 0 0         if ( defined($name) ) {
522 0 0         if ( !defined( $VMs->{$name} ) ) {
523 0           die( '"' . $VMs . '" is not a known VM' );
524             }
525 0           push( @VM_names, $name );
526             } else {
527 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
528             }
529 0           foreach my $name (@VM_names) {
530 0           print "Stopping " . $name . "...\n";
531 0           my @args = ( 'virsh', 'destroy', $name );
532 0 0         system(@args) == 0 or warn("system '@args' failed... $?");
533             }
534             } ## end sub stop_clones
535              
536             =head2 vm_list
537              
538             Generate a list of VMs.
539              
540             =cut
541              
542             sub vm_list {
543 0     0 1   my $self = $_[0];
544              
545             # only need to create this all once
546 0 0         if ( defined( $self->{VMs} ) ) {
547 0           return $self->{VMs};
548             }
549              
550 0           my $VMs = {};
551              
552 0           my $current = $self->{start};
553 0           my $till = $current + $self->{count} - 1;
554 0           while ( $current <= $till ) {
555 0           my $name = $self->{clone_name_base} . $current;
556 0           my $hex = sprintf( '%#x', $current );
557 0           $hex =~ s/^0[Xx]//;
558              
559             $VMs->{$name} = {
560             ip => $self->{ipv4_base} . $current,
561 0           mac => $self->{mac_base} . $hex,
562             };
563              
564 0           $current++;
565             } ## end while ( $current <= $till )
566              
567 0           return $VMs;
568             } ## end sub vm_list
569              
570             =head1 BLANKED MS DOMAINS
571              
572             microsoft.com
573             windowsupdate.com
574             windows.com
575             microsoft.com.nsatc.net
576             bing.net
577             live.com
578             cloudapp.net
579             cs1.wpc.v0cdn.net
580             -msedge.net
581             msedge.net
582             microsoft.com.akadns.net
583             footprintpredict.com
584             microsoft-hohm.com
585             msn.com
586             social.ms.akadns.net
587             msedge.net
588             dc-msedge.net
589             bing.com
590             edgekey.net
591             azureedge.net
592             amsn.net
593             moiawsorigin.clo.footprintdns.com
594             office365.com
595             skype.com
596             trafficmanager.net
597              
598             =head1 AUTHOR
599              
600             Zane C. Bowers-Hadley, C<< >>
601              
602             =head1 BUGS
603              
604             Please report any bugs or feature requests to C, or through
605             the web interface at L. I will be notified, and then you'll
606             automatically be notified of progress on your bug as I make changes.
607              
608              
609              
610              
611             =head1 SUPPORT
612              
613             You can find documentation for this module with the perldoc command.
614              
615             perldoc VM::Libvirt::CloneHelper
616              
617              
618             You can also look for information at:
619              
620             =over 4
621              
622             =item * RT: CPAN's request tracker (report bugs here)
623              
624             L
625              
626             =item * Search CPAN
627              
628             L
629              
630             =back
631              
632              
633             =head1 ACKNOWLEDGEMENTS
634              
635              
636             =head1 LICENSE AND COPYRIGHT
637              
638             This software is Copyright (c) 2022 by Zane C. Bowers-Hadley.
639              
640             This is free software, licensed under:
641              
642             The Artistic License 2.0 (GPL Compatible)
643              
644              
645             =cut
646              
647             1; # End of VM::Libvirt::CloneHelper