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   57166 use 5.006;
  1         4  
4 1     1   4 use strict;
  1         2  
  1         30  
5 1     1   8 use warnings;
  1         1  
  1         31  
6 1     1   484 use File::Slurp qw(write_file read_file);
  1         28858  
  1         52  
7 1     1   7 use File::Temp;
  1         2  
  1         1972  
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.0
16              
17             =cut
18              
19             our $VERSION = '0.1.0';
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             }
149             elsif ( $key eq 'ipv4_base' ) {
150              
151             # make sure we have a likely sane base for the IPv4 address
152 0 0         if ( $args{ipv4_base} !~ /^[0-9]+\.[0-9]+\.[0-9]+\.$/ ) {
153 0           die( '"' . $args{ipv4_base} . '" does not appear to be a valid base for a IPv4 address' );
154             }
155             }
156             elsif ( $key eq 'to_clone' ) {
157              
158             # make sure we have a likely sane base VM name
159 0 0         if ( $args{to_clone} !~ /^[A-Za-z0-9\-\.]+$/ ) {
160 0           die( '"' . $args{to_clone} . '" does not appear to be a valid VM name' );
161             }
162             }
163             elsif ( $key eq 'clone_name_base' ) {
164              
165             # make sure we have a likely sane base name to use for creating clones
166 0 0         if ( $args{clone_name_base} !~ /^[A-Za-z0-9\-\.]+$/ ) {
167 0           die( '"' . $args{clone_name_base} . '" does not appear to be a valid VM name' );
168             }
169             }
170              
171             # likely good, adding
172 0           $self->{$key} = $args{$key};
173             }
174              
175 0           $self->{end} = $self->{start} + $self->{count} - 1;
176              
177 0           $self->{VMs} = $self->vm_list;
178              
179 0           return $self;
180             }
181              
182             =head2 clone
183              
184             Create the clones.
185              
186             One optional argument is taken and that is the VM to operate on.
187             Otherwise all is ran for them all.
188              
189             $clone_helper->clone;
190              
191             =cut
192              
193             sub clone {
194 0     0 1   my $self = $_[0];
195 0           my $name = $_[1];
196              
197 0           my $VMs = $self->vm_list;
198              
199 0           my @VM_names;
200 0 0         if ( defined($name) ) {
201 0 0         if ( !defined( $VMs->{$name} ) ) {
202 0           die( '"' . $VMs . '" is not a known VM' );
203             }
204 0           push( @VM_names, $name );
205             }
206             else {
207 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
208             }
209 0           foreach my $name (@VM_names) {
210             print "Cloning '"
211             . $self->{to_clone}
212             . "' to '"
213             . $name . "'("
214             . $VMs->{$name}{mac} . ", "
215             . $VMs->{$name}{ip}
216 0           . ")...\n";
217              
218 0           my @args = ( 'virt-clone', '-m', $VMs->{$name}{mac}, '-o', $self->{to_clone}, '--auto-clone', '-n', $name );
219 0 0         system(@args) == 0 or die("system '@args' failed... $?");
220             }
221             }
222              
223             =head2 delete_clones
224              
225             Delete all the clones
226              
227             One optional argument is taken and that is the VM to operate on.
228             Otherwise all is ran for them all.
229              
230             $clone_helper->delete_clones;
231              
232             =cut
233              
234             sub delete_clones {
235 0     0 1   my $self = $_[0];
236 0           my $name = $_[1];
237              
238             # virsh undefine --snapshots-metadata
239             # the VM under /var/lib/libvirt/images needs to be removed manually given
240             # the shit show that is libvirt does not have a means of sanely removing
241             # VMs and relevant storage... for example it will include ISOs in relevant
242             # VMs to be removed if you let it... and it is likely to fail to remove the
243             # base disk image for a VM, even if you pass it any/every combination of
244             # possible flags...
245              
246 0           my $VMs = $self->vm_list;
247              
248 0           my @VM_names;
249 0 0         if ( defined($name) ) {
250 0 0         if ( !defined( $VMs->{$name} ) ) {
251 0           die( '"' . $VMs . '" is not a known VM' );
252             }
253 0           push( @VM_names, $name );
254             }
255             else {
256 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
257             }
258 0           foreach my $name (@VM_names) {
259 0           print "Undefining " . $name . "\n";
260 0           my @args = ( 'virsh', 'undefine', '--snapshots-metadata', $name );
261 0 0         system(@args) == 0 or warn("system '@args' failed... $?");
262              
263 0           my $image = '/var/lib/libvirt/images/' . $name . '.qcow2';
264              
265 0 0         if ( -f $image ) {
266 0           print "Unlinking " . $image . "\n";
267 0 0         unlink($image) or die( 'unlinking "' . $image . '" failed... ' . $! );
268             }
269             }
270             }
271              
272             =head2 net_xml
273              
274             Returns a string with the full net config XML.
275              
276             my $net_config_xml=$clone_helper->net_xml;
277             print $net_config_xml;
278              
279             =cut
280              
281             sub net_xml {
282 0     0 1   my $self = $_[0];
283              
284 0           my $VMs = $self->vm_list;
285              
286 0 0         my $xml = read_file( $self->{net_head} ) or die( 'Failed to read "' . $self->{net_head} . '"' );
287 0 0         my $xml_tail = read_file( $self->{net_tail} ) or die( 'Failed to read "' . $self->{net_tail} . '"' );
288              
289 0 0         if ( $self->{windows_blank} ) {
290 0           $xml = $xml . '
291            
292            
293            
294            
295            
296            
297            
298            
299            
300            
301            
302            
303            
304            
305            
306            
307            
308            
309            
310            
311            
312            
313            
314            
315            
316             ';
317             }
318              
319 0 0         if ( -f $self->{blank_domains} ) {
320 0 0         my $blank_raw = read_file( $self->{blank_domains} ) or die( 'Failed to read "' . $self->{blank_domains} . '"' );
321              
322             # remove any blank lines or anyhting commented out
323 0           my @blank_split = grep( !/^[\ \t]*]$/, grep( !/^[\ \t]*#/, split( /\n/, $blank_raw ) ) );
324 0           foreach my $line (@blank_split) {
325 0           chomp($line);
326 0           $line =~ s/^[\ \t]*//;
327 0           $line =~ s/[\ \t]*$//;
328 0           foreach my $domain ( split( /[\ \t]+/, $line ) ) {
329 0           $xml = $xml . " \n";
330             }
331             }
332             }
333              
334 0           my @VM_names = sort( keys( %{$VMs} ) );
  0            
335 0           foreach my $name (@VM_names) {
336             $xml
337             = $xml
338             . '
339             . $VMs->{$name}{mac} . ','
340 0           . $VMs->{$name}{ip} . '\'/>' . "\n";
341             }
342              
343 0           return $xml . $xml_tail;
344             }
345              
346             =head2 net_redefine
347              
348             Redefines the network in question.
349              
350             =cut
351              
352             sub net_redefine {
353 0     0 1   my $self = $_[0];
354              
355 0           my $xml = $self->net_xml;
356              
357 0           print "Undefining the the network('" . $self->{net} . "') for readding it...\n";
358 0           my @args = ( 'virsh', 'net-undefine', $self->{net} );
359 0 0         system(@args) == 0 or die("system '@args' failed... $?");
360              
361 0           my $fh = File::Temp->new;
362 0           my $tmp_file = $fh->filename;
363              
364 0 0         write_file( $tmp_file, $xml ) or die( 'Failed to write tmp net config to "' . $tmp_file . '"... ' . $@ );
365              
366 0           print "Defining the the network('" . $self->{net} . "') for readding it...\n";
367 0           @args = ( 'virsh', 'net-define', '--file', $tmp_file );
368 0 0         system(@args) == 0 or die("system '@args' failed... $?");
369              
370 0 0         unlink($tmp_file) or die( 'Failed to unlink net config "' . $tmp_file . '"... ' . $@ );
371              
372 0           return;
373             }
374              
375             =head2 recreate
376              
377             Recreate the specified VM.
378              
379             One optional argument is taken and that is the VM to operate on.
380             Otherwise all is ran for them all.
381              
382             If you wish to recreate all, you should likely use recreate_all, to avoid
383             any issues caused by starting them all at the same time.
384              
385             $clone_helper->recreate('foo100');
386              
387             =cut
388              
389             sub recreate {
390 0     0 1   my $self = $_[0];
391 0           my $name = $_[0];
392              
393 0 0         if ( !defined($name) ) {
394 0           die('No VM specified to recreate');
395             }
396              
397 0           my $VMs = $self->vm_list;
398              
399 0 0         if ( !defined( $VMs->{$name} ) ) {
400 0           die( '"' . $VMs . '" is not a known VM' );
401             }
402              
403 0           $self->delete_clones($name);
404 0           $self->clone($name);
405 0           $self->start_clones($name);
406 0           sleep($self->{wait});
407 0           $self->snapshot_clones($name);
408 0           $self->stop_clones($name);
409              
410 0           return;
411             }
412              
413             =head2 recreate_all
414              
415             Recreate all VMs.
416              
417             Does one at a time.
418              
419             $clone_helper->recreate_all;
420              
421             =cut
422              
423             sub recreate_all {
424 0     0 1   my $self = $_[0];
425              
426 0           my $VMs = $self->vm_list;
427              
428 0           my @VM_names = sort( keys( %{$VMs} ) );
  0            
429 0           foreach my $name (@VM_names) {
430 0           $self->delete_clones($name);
431 0           $self->clone($name);
432 0           $self->start_clones($name);
433 0           sleep($self->{wait});
434 0           $self->snapshot_clones($name);
435 0           $self->stop_clones($name);
436             }
437              
438 0           return;
439             }
440              
441             =head2 snapshot_clones
442              
443             Snapshot all the clones
444              
445             One optional argument is taken and that is the VM to operate on.
446             Otherwise all is ran for them all.
447              
448             $clone_helper->snapshot_clones;
449              
450             =cut
451              
452             sub snapshot_clones {
453 0     0 1   my $self = $_[0];
454 0           my $name = $_[1];
455              
456 0           my $VMs = $self->vm_list;
457              
458 0           my @VM_names;
459 0 0         if ( defined($name) ) {
460 0 0         if ( !defined( $VMs->{$name} ) ) {
461 0           die( '"' . $VMs . '" is not a known VM' );
462             }
463 0           push( @VM_names, $name );
464             }
465             else {
466 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
467             }
468 0           foreach my $name (@VM_names) {
469 0           print "Snapshotting " . $name . "...\n";
470 0           my @args = ( 'virsh', 'snapshot-create-as', '--name', $self->{snapshot_name}, $name );
471 0 0         system(@args) == 0 or die("system '@args' failed... $?");
472             }
473             }
474              
475             =head2 start_clones
476              
477             Start all the clones
478              
479             One optional argument is taken and that is the VM to operate on.
480             Otherwise all is ran for them all.
481              
482             $clone_helper->start_clones;
483              
484             =cut
485              
486             sub start_clones {
487 0     0 1   my $self = $_[0];
488 0           my $name = $_[1];
489              
490 0           my $VMs = $self->vm_list;
491              
492 0           my @VM_names;
493 0 0         if ( defined($name) ) {
494 0 0         if ( !defined( $VMs->{$name} ) ) {
495 0           die( '"' . $VMs . '" is not a known VM' );
496             }
497 0           push( @VM_names, $name );
498             }
499             else {
500 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
501             }
502 0           foreach my $name (@VM_names) {
503 0           print "Starting " . $name . "...\n";
504 0           my @args = ( 'virsh', 'start', $name );
505 0 0         system(@args) == 0 or die("system '@args' failed... $?");
506             }
507             }
508              
509             =head2 stop_clones
510              
511             Stop all the clones. This does not stop them gracefully as we don't
512             need to as they are being started via snapshot.
513              
514             One optional argument is taken and that is the VM to operate on.
515             Otherwise all is ran for them all.
516              
517             $clone_helper->stop_clones;
518              
519             =cut
520              
521             sub stop_clones {
522 0     0 1   my $self = $_[0];
523 0           my $name = $_[1];
524              
525 0           my $VMs = $self->vm_list;
526              
527 0           my @VM_names;
528 0 0         if ( defined($name) ) {
529 0 0         if ( !defined( $VMs->{$name} ) ) {
530 0           die( '"' . $VMs . '" is not a known VM' );
531             }
532 0           push( @VM_names, $name );
533             }
534             else {
535 0           @VM_names = sort( keys( %{$VMs} ) );
  0            
536             }
537 0           foreach my $name (@VM_names) {
538 0           print "Stopping " . $name . "...\n";
539 0           my @args = ( 'virsh', 'destroy', $name );
540 0 0         system(@args) == 0 or warn("system '@args' failed... $?");
541             }
542             }
543              
544             =head2 vm_list
545              
546             Generate a list of VMs.
547              
548             =cut
549              
550             sub vm_list {
551 0     0 1   my $self = $_[0];
552              
553             # only need to create this all once
554 0 0         if ( defined( $self->{VMs} ) ) {
555 0           return $self->{VMs};
556             }
557              
558 0           my $VMs = {};
559              
560 0           my $current = $self->{start};
561 0           my $till = $current + $self->{count} - 1;
562 0           while ( $current <= $till ) {
563 0           my $name = $self->{clone_name_base} . $current;
564 0           my $hex = sprintf( '%#x', $current );
565 0           $hex =~ s/^0[Xx]//;
566              
567             $VMs->{$name} = {
568             ip => $self->{ipv4_base} . $current,
569 0           mac => $self->{mac_base} . $hex,
570             };
571              
572 0           $current++;
573             }
574              
575 0           return $VMs;
576             }
577              
578             =head1 BLANKED MS DOMAINS
579              
580             microsoft.com
581             windowsupdate.com
582             windows.com
583             microsoft.com.nsatc.net
584             bing.net
585             live.com
586             cloudapp.net
587             cs1.wpc.v0cdn.net
588             -msedge.net
589             msedge.net
590             microsoft.com.akadns.net
591             footprintpredict.com
592             microsoft-hohm.com
593             msn.com
594             social.ms.akadns.net
595             msedge.net
596             dc-msedge.net
597             bing.com
598             edgekey.net
599             azureedge.net
600             amsn.net
601             moiawsorigin.clo.footprintdns.com
602             office365.com
603             skype.com
604             trafficmanager.net
605              
606             =head1 AUTHOR
607              
608             Zane C. Bowers-Hadley, C<< >>
609              
610             =head1 BUGS
611              
612             Please report any bugs or feature requests to C, or through
613             the web interface at L. I will be notified, and then you'll
614             automatically be notified of progress on your bug as I make changes.
615              
616              
617              
618              
619             =head1 SUPPORT
620              
621             You can find documentation for this module with the perldoc command.
622              
623             perldoc VM::Libvirt::CloneHelper
624              
625              
626             You can also look for information at:
627              
628             =over 4
629              
630             =item * RT: CPAN's request tracker (report bugs here)
631              
632             L
633              
634             =item * CPAN Ratings
635              
636             L
637              
638             =item * Search CPAN
639              
640             L
641              
642             =back
643              
644              
645             =head1 ACKNOWLEDGEMENTS
646              
647              
648             =head1 LICENSE AND COPYRIGHT
649              
650             This software is Copyright (c) 2022 by Zane C. Bowers-Hadley.
651              
652             This is free software, licensed under:
653              
654             The Artistic License 2.0 (GPL Compatible)
655              
656              
657             =cut
658              
659             1; # End of VM::Libvirt::CloneHelper