File Coverage

blib/lib/VM/Libvirt/CloneHelper.pm
Criterion Covered Total %
statement 14 121 11.5
branch 0 50 0.0
condition n/a
subroutine 5 14 35.7
pod 9 9 100.0
total 28 194 14.4


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