File Coverage

lib/Illumos/Zones.pm
Criterion Covered Total %
statement 10 186 5.3
branch 0 90 0.0
condition 0 21 0.0
subroutine 4 28 14.2
pod 25 26 96.1
total 39 351 11.1


line stmt bran cond sub pod time code
1             package Illumos::Zones;
2              
3 1     1   569 use strict;
  1         1  
  1         23  
4 1     1   4 use warnings;
  1         2  
  1         3987  
5              
6             # version
7             our $VERSION = '0.1.7';
8              
9             # commands
10             my $ZONEADM = '/usr/sbin/zoneadm';
11             my $ZONECFG = '/usr/sbin/zonecfg';
12             my $ZONENAME = '/usr/bin/zonename';
13              
14             my %ZMAP = (
15             zoneid => 0,
16             zonename => 1,
17             state => 2,
18             zonepath => 3,
19             uuid => 4,
20             brand => 5,
21             'ip-type' => 6,
22             );
23              
24             # properties that can only be set on creation
25             my @CREATEPROP = qw(zonename zonepath brand ip-type);
26             my @LXNETPROPS = qw(gateway ips primary);
27              
28             my $regexp = sub {
29             my $rx = shift;
30             my $msg = shift;
31              
32             return sub {
33             my $value = shift;
34             return $value =~ /$rx/ ? undef : "$msg ($value)";
35             }
36             };
37              
38             my $elemOf = sub {
39             my $elems = [ @_ ];
40              
41             return sub {
42             my $value = shift;
43             return (grep { $_ eq $value } @$elems) ? undef
44             : 'expected a value from the list: ' . join(', ', @$elems);
45             }
46             };
47              
48             my $getBrands = sub {
49             my @brands = ();
50             for (glob('/usr/lib/brand/*/config.xml')) {
51             open my $fh, '<', $_ or next;
52             while (<$fh>) {
53             /
54             push @brands, $1;
55             last;
56             }
57             close $fh;
58             }
59             return \@brands;
60             };
61              
62             my $TEMPLATE = {
63             zonename => '',
64             zonepath => '',
65             brand => 'lipkg',
66             'ip-type' => 'exclusive',
67             };
68              
69             my $SCHEMA = {
70             zonename => {
71             description => 'name of zone',
72             validator => $regexp->(qr/^[-\w]+$/, 'zonename not valid'),
73             },
74             zonepath => {
75             description => 'path to zone root',
76             example => '"zonepath" : "/zones/mykvm"',
77             validator => $regexp->(qr/^\/[-\w\/]+$/, 'zonepath is not a valid path'),
78             },
79             autoboot => {
80             optional => 1,
81             description => 'boot zone automatically',
82             validator => $elemOf->(qw(true false)),
83             },
84             bootargs => {
85             optional => 1,
86             description => 'boot arguments for zone',
87             validator => sub { return undef },
88             },
89             pool => {
90             optional => 1,
91             description => 'name of the resource pool this zone must be bound to',
92             validator => sub { return undef },
93             },
94             limitpriv => {
95             description => 'the maximum set of privileges any process in this zone can obtain',
96             default => 'default',
97             validator => $regexp->(qr/^[-\w,]+$/, 'limitpriv not valid'),
98             },
99             brand => {
100             description => "the zone's brand type",
101             default => 'lipkg',
102             validator => $elemOf->(@{$getBrands->()}),
103             },
104             'ip-type' => {
105             description => 'ip-type of zone. can either be "exclusive" or "shared"',
106             default => 'exclusive',
107             validator => $elemOf->(qw(exclusive shared)),
108             },
109             hostid => {
110             optional => 1,
111             description => 'emulated 32-bit host identifier',
112             validator => $regexp->(qr/^(?:[\da-f]{1,8}|)$/i, 'hostid not valid'),
113             },
114             'cpu-shares' => {
115             optional => 1,
116             description => 'the number of Fair Share Scheduler (FSS) shares',
117             validator => $regexp->(qr/^\d+$/, 'cpu-shares not valid'),
118             },
119             'max-lwps' => {
120             optional => 1,
121             description => 'the maximum number of LWPs simultaneously available',
122             validator => $regexp->(qr/^\d+$/, 'max-lwps not valid'),
123             },
124             'max-msg-ids' => {
125             optional => 1,
126             description => 'the maximum number of message queue IDs allowed',
127             validator => $regexp->(qr/^\d+$/, 'max-msg-ids not valid'),
128             },
129             'max-sem-ids' => {
130             optional => 1,
131             description => 'the maximum number of semaphore IDs allowed',
132             validator => $regexp->(qr/^\d+$/, 'max-sem-ids not valid'),
133             },
134             'max-shm-ids' => {
135             optional => 1,
136             description => 'the maximum number of shared memory IDs allowed',
137             validator => $regexp->(qr/^\d+$/, 'max-shm-ids not valid'),
138             },
139             'max-shm-memory' => {
140             optional => 1,
141             description => 'the maximum amount of shared memory allowed',
142             validator => $regexp->(qr/^\d+[KMGT]?$/i, 'max-shm-memory not valid'),
143             },
144             'scheduling-class' => {
145             optional => 1,
146             description => 'Specifies the scheduling class used for processes running',
147             validator => sub { return undef },
148             },
149             'fs-allowed' => {
150             optional => 1,
151             description => 'a comma-separated list of additional filesystems that may be mounted',
152             validator => $regexp->(qr/^(?:[-\w,]+|)$/, 'fs-allowed not valid'),
153             },
154             attr => {
155             optional => 1,
156             array => 1,
157             description => 'generic attributes',
158             members => {
159             name => {
160             description => 'attribute name',
161             validator => sub { return undef },
162             },
163             type => {
164             description => 'attribute type',
165             validator => sub { return undef },
166             },
167             value => {
168             description => 'attribute value',
169             validator => sub { return undef },
170             },
171             },
172             },
173             'capped-cpu' => {
174             optional => 1,
175             description => 'limits for CPU usage',
176             members => {
177             ncpus => {
178             description => 'sets the limit on the amount of CPU time. value is the percentage of a single CPU',
179             validator => $regexp->(qr/^(?:\d*\.\d+|\d+\.\d*)$/, 'ncpus value not valid. check man zonecfg'),
180             },
181             },
182             },
183             'capped-memory' => {
184             optional => 1,
185             description => 'limits for physical, swap, and locked memory',
186             members => {
187             physical => {
188             optional => 1,
189             description => 'limits of physical memory. can be suffixed by (K, M, G, T)',
190             validator => $regexp->(qr/^\d+[KMGT]?$/i, 'physical capped-memory is not valid. check man zonecfg'),
191             },
192             swap => {
193             optional => 1,
194             description => 'limits of swap memory. can be suffixed by (K, M, G, T)',
195             validator => $regexp->(qr/^\d+[KMGT]?$/i, 'swap capped-memory is not valid. check man zonecfg'),
196             },
197             locked => {
198             optional => 1,
199             description => 'limits of locked memory. can be suffixed by (K, M, G, T)',
200             validator => $regexp->(qr/^\d+[KMGT]?$/i, 'locked capped-memory is not valid. check man zonecfg'),
201             },
202             },
203             },
204             dataset => {
205             optional => 1,
206             array => 1,
207             description => 'ZFS dataset',
208             members => {
209             name => {
210             description => 'the name of a ZFS dataset to be accessed from within the zone',
211             validator => $regexp->(qr/^\w[-\w\/]+$/, 'dataset name not valid. check man zfs'),
212             },
213             },
214             },
215             'dedicated-cpu' => {
216             optional => 1,
217             description => "subset of the system's processors dedicated to this zone while it is running",
218             members => {
219             ncpus => {
220             description => "the number of cpus that should be assigned for this zone's exclusive use",
221             validator => $regexp->(qr/^\d+(?:-\d+)?$/, 'dedicated-cpu ncpus not valid. check man zonecfg'),
222             },
223             importance => {
224             optional => 1,
225             description => 'specifies the pset.importance value for use by poold',
226             validator => sub { return undef },
227             },
228             },
229             },
230             device => {
231             optional => 1,
232             array => 1,
233             description => 'device',
234             members => {
235             match => {
236             description => 'device name to match',
237             validator => sub { return undef },
238             },
239             },
240             },
241             fs => {
242             optional => 1,
243             array => 1,
244             description => 'file-system',
245             members => {
246             dir => {
247             description => 'directory of the mounted filesystem',
248             validator => $regexp->(qr/^\/[-\w\/\.]+$/, 'dir is not a valid directory'),
249             },
250             special => {
251             description => 'path of fs to be mounted',
252             validator => $regexp->(qr/^[-\w\/\.]+$/, 'special is not valid'),
253             },
254             raw => {
255             optional => 1,
256             description => 'path of raw disk',
257             validator => $regexp->(qr/^\/[-\w\/]+$/, 'raw is not valid'),
258             },
259             type => {
260             description => 'type of fs',
261             validator => $elemOf->(qw(lofs zfs)),
262             },
263             options => {
264             optional => 1,
265             description => 'mounting options',
266             validator => $regexp->(qr/^\[[\w,]*\]$/, 'options not valid'),
267             },
268             },
269             },
270             net => {
271             optional => 1,
272             array => 1,
273             description => 'network interface',
274             members => {
275             address => {
276             optional => 1,
277             description => 'IP address of network interface',
278             validator => $regexp->(qr/^\d{1,3}(?:\.\d{1,3}){3}(?:\/\d{1,2})?$/, 'IP address not valid'),
279             },
280             physical => {
281             description => 'network interface',
282             validator => $regexp->(qr/^[-\w]+/, 'physical not valid'),
283             },
284             defrouter => {
285             optional => 1,
286             description => 'IP address of default router',
287             validator => $regexp->(qr/^\d{1,3}(?:\.\d{1,3}){3}$/, 'IP address not valid'),
288             },
289             ips => {
290             optional => 1,
291             array => 1,
292             description => 'IPs for LX zones',
293             validator => $regexp->(qr/^\d{1,3}(?:\.\d{1,3}){3}(?:\/\d{1,2})$/, 'Not a valid CIDR IP address'),
294             },
295             gateway => {
296             optional => 1,
297             description => 'Gateway for LX zones',
298             validator => $regexp->(qr/^\d{1,3}(?:\.\d{1,3}){3}$/, 'IP address not valid'),
299             },
300             primary => {
301             optional => 1,
302             description => 'Primary Interface for LX zones',
303             validator => $elemOf->(qw(true false)),
304             },
305             },
306             },
307             rctl => {
308             optional => 1,
309             array => 1,
310             description => 'resource control',
311             members => {
312             name => {
313             description => 'resource name',
314             validator => sub { return undef },
315             },
316             value => {
317             description => 'resource value',
318             validator => sub { return undef },
319             },
320             },
321             },
322             };
323              
324             # private methods
325             my $RESOURCES = sub {
326             return [ map { $SCHEMA->{$_}->{members} ? $_ : () } keys %$SCHEMA ];
327             };
328              
329             my $resIsArray = sub {
330             my $self = shift;
331             my $res = shift;
332              
333             return $SCHEMA->{$res}->{array};
334             };
335              
336             my $RESARRAYS = sub {
337             return [ map { $SCHEMA->{$_}->{array} ? $_ : () } @{$RESOURCES->()} ];
338             };
339              
340             my $zoneCmd = sub {
341             my $self = shift;
342             my $zoneName = shift;
343             my $cmd = shift;
344             my @opts = @_;
345              
346             my @cmd = ($ZONEADM, '-z', $zoneName, $cmd, @opts);
347              
348             print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
349             system(@cmd) and die "ERROR: cannot $cmd zone $zoneName\n";
350             };
351              
352             my $encodeLXnetProp = sub {
353             my $self = shift;
354             my $prop = shift;
355             my $value = shift;
356              
357             $value = ref $value eq 'ARRAY' ? "(name=$prop,value=\"" . join (',', @$value) . '")'
358             : "(name=$prop,value=\"$value\")";
359             $prop = 'property';
360              
361             return ($prop, $value);
362             };
363              
364             my $decodeLXnetProp = sub {
365             my $self = shift;
366             my $prop = shift;
367             my $value = shift;
368              
369             return ($prop, $value) if !($prop eq 'property');
370              
371             ($prop) = $value =~ /name=(\w+)/;
372             my @values = split /,/, ($value =~ /value="([^"]+)"/)[0];
373             if (!$SCHEMA->{net}->{members}->{$prop}->{array}) {
374             return ($prop, $values[0]);
375             }
376             return ($prop, [ @values ]);
377             };
378              
379             # constructor
380             sub new {
381 1     1 0 355 my $class = shift;
382 1         2 my $self = { @_ };
383 1         2 return bless $self, $class
384             }
385              
386             # public methods
387             sub schema {
388 1     1 1 8131 return $SCHEMA;
389             }
390              
391             sub template {
392 0     0 1   return $TEMPLATE;
393             }
394              
395             sub resources {
396 0     0 1   return $RESOURCES->();
397             }
398              
399             sub resourceArrays {
400 0     0 1   return $RESARRAYS->();
401             }
402              
403             # zoneName is a static method
404             sub zoneName {
405 0     0 1   my @cmd = ($ZONENAME);
406              
407 0 0         open my $zones, '-|', @cmd
408             or die "ERROR: cannot get zonename\n";
409              
410 0           chomp (my $zonename = <$zones>);
411              
412 0           return $zonename;
413             }
414              
415             # isGZ is a static method
416             sub isGZ {
417 0     0 1   return zoneName() eq 'global';
418             }
419              
420             sub listZones {
421 0     0 1   my $self = shift;
422 0           my $opts = shift;
423              
424 0           my @cmd = ($ZONEADM, qw(list -cp));
425              
426 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
427 0 0         open my $zones, '-|', @cmd
428             or die "ERROR: cannot get list of Zones\n";
429              
430 0           my $zoneList = [];
431 0           while (my $zone = <$zones>) {
432 0           chomp $zone;
433 0           my $zoneCfg = { map { $_ => (split /:/, $zone)[$ZMAP{$_}] } keys %ZMAP };
  0            
434             # apply brand and SMF filter
435 0 0 0       next if $opts->{brandFilter} && $zoneCfg->{brand} !~ /$opts->{brandFilter}/;
436             next if $opts->{requireSMF} && $zoneCfg->{zonename} ne 'global'
437 0 0 0       && !-f $zoneCfg->{zonepath} . '/root/etc/svc/repository.db';
      0        
438              
439 0           push @$zoneList, $zoneCfg;
440             }
441              
442 0           return $zoneList;
443             }
444              
445             sub listZone {
446 0     0 1   my $self = shift;
447 0           my $zoneName = shift;
448 0           my $opts = shift;
449              
450 0           my ($zone) = grep { $_->{zonename} eq $zoneName } @{$self->listZones($opts)};
  0            
  0            
451              
452 0           return $zone;
453             }
454              
455             sub zoneState {
456 0     0 1   my $self = shift;
457 0           my $zoneName = shift;
458 0           my $opts = shift;
459              
460 0           my $zone = $self->listZone($zoneName, $opts);
461              
462 0 0         return $zone ? $zone->{state} : undef;
463             }
464              
465             sub boot {
466 0     0 1   my $self = shift;
467              
468 0           $self->$zoneCmd(shift, 'boot');
469             }
470              
471             sub shutdown {
472 0     0 1   my $self = shift;
473 0           my $zoneName = shift;
474 0 0         my @reboot = $_[0] ? qw(-r) : ();
475              
476 0           $self->$zoneCmd($zoneName, 'shutdown', @reboot);
477             }
478              
479             sub reboot {
480 0     0 1   my $self = shift;
481              
482 0           $self->shutdown(shift, 1);
483             };
484              
485             sub createZone {
486 0     0 1   my $self = shift;
487 0           my $zoneName = shift;
488 0           my $props = shift;
489              
490 0           my @cmd = ($ZONECFG, '-z', $zoneName, qw(create -b ;));
491              
492 0           for my $prop (keys %$props) {
493 0           push @cmd, ('set', $prop, '=', $props->{$prop}, ';');
494             }
495              
496 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
497 0 0         system(@cmd) and die "ERROR: cannot create zone $zoneName\n";
498             }
499              
500             sub deleteZone {
501 0     0 1   my $self = shift;
502 0           my $zoneName = shift;
503              
504 0           my @cmd = ($ZONECFG, '-z', $zoneName, 'delete');
505              
506 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
507 0 0         system(@cmd) and die "ERROR: cannot delete zone $zoneName\n";
508             }
509              
510             sub installZone {
511 0     0 1   my $self = shift;
512 0           my $zoneName = shift;
513 0           my $img = shift;
514              
515 0 0         $self->$zoneCmd($zoneName, 'install', ($img ? ('-s', $img) : ()));
516             }
517              
518             sub uninstallZone {
519 0     0 1   my $self = shift;
520              
521 0           $self->$zoneCmd(shift, 'uninstall');
522             }
523              
524             sub zoneExists {
525 0     0 1   my $self = shift;
526 0           my $zoneName = shift;
527 0           my $opts = shift;
528              
529 0 0         return $self->listZone($zoneName, $opts) ? 1 : 0;
530             }
531              
532             sub getZoneProperties {
533 0     0 1   my $self = shift;
534 0           my $zoneName = shift;
535 0           my $properties = {};
536              
537 0 0         return {} if !$self->zoneExists($zoneName);
538              
539 0           my @cmd = ($ZONECFG, '-z', $zoneName, 'info');
540              
541 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
542 0 0         open my $props, '-|', @cmd
543             or die "ERROR: cannot get properties of zone '$zoneName'\n";
544              
545 0           my $resName;
546 0           while (<$props>) {
547 0           chomp;
548             # remove square brackets at beginning and end of line
549 0 0         s/^(\s*)\[/$1/ && s/\]\s*//;
550 0           my ($isres, $property, $value) = /^(\s+)?([^:]+):(?:\s+(.*))?$/;
551             # at least property must be valid
552 0 0         $property or next;
553              
554 0 0 0       if (defined $isres && length $isres > 0) {
555             # transform net properties for LX zones
556 0 0         ($property, $value) = $self->$decodeLXnetProp($property, $value) if $resName eq 'net';
557              
558             # check if property exists in schema
559 0 0         grep { $_ eq $property } keys %{$SCHEMA->{$resName}->{members}} or next;
  0            
  0            
560 0 0         if ($self->$resIsArray($resName)) {
561 0           $properties->{$resName}->[-1]->{$property} = $value;
562             }
563             else {
564 0           $properties->{$resName}->{$property} = $value;
565             }
566             }
567             else {
568             # check if property exists in schema
569 0 0         grep { $_ eq $property } keys %$SCHEMA or next;
  0            
570             # check if property is a resource
571 0 0         grep { $_ eq $property } @{$RESOURCES->()} and do {
  0            
  0            
572 0           $resName = $property;
573 0 0         if ($self->$resIsArray($property)) {
574 0           push @{$properties->{$property}}, {};
  0            
575             }
576 0           next;
577             };
578 0           $properties->{$property} = $value;
579             }
580             }
581            
582 0           return $properties;
583             }
584              
585             sub setZoneProperties {
586 0     0 1   my $self = shift;
587 0           my $zoneName = shift;
588 0           my $props = shift;
589 0           my $img = shift;
590 0           my $oldProps = $self->getZoneProperties($zoneName);
591              
592             $self->zoneExists($zoneName) || $self->createZone($zoneName,
593 0 0         { map { $_ => $props->{$_} } @CREATEPROP });
  0            
594              
595             # remove props that cannot be changed after creation
596 0           delete $props->{$_} for @CREATEPROP;
597              
598 0           my $state = $self->zoneState($zoneName);
599 0 0         $self->installZone($zoneName, $img) if $state eq 'configured';
600              
601             # clean up all resources
602 0           $self->clearResources($zoneName);
603              
604 0           for my $prop (keys %$props) {
605 0 0         if (ref $props->{$prop} eq 'ARRAY') {
    0          
606 0           for my $elem (@{$props->{$prop}}) {
  0            
607 0           $self->addResource($zoneName, $prop, $elem);
608             }
609             }
610 0           elsif (grep { $_ eq $prop } @{$RESOURCES->()}) {
  0            
611 0           $self->addResource($zoneName, $prop, $props->{$prop});
612             }
613             else {
614 0 0 0       next if $oldProps->{$prop} && $oldProps->{$prop} eq $props->{$prop};
615 0 0         if ($props->{$prop}) {
616 0           $self->setProperty($zoneName, $prop, $props->{$prop});
617             }
618             else {
619 0           $self->clearProperty($zoneName, $prop);
620             }
621             }
622             }
623             }
624              
625             sub resourceExists {
626 0     0 1   my $self = shift;
627 0           my $zoneName = shift;
628 0           my $resource = shift;
629 0           my $property = shift;
630 0           my $value = shift;
631              
632 0           my @cmd = ($ZONECFG, '-z', $zoneName, 'info', $resource);
633              
634 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
635 0 0         open my $res, '-|', @cmd
636             or die "ERROR: cannot get resource '$resource' of zone '$zoneName'\n";
637              
638 0           chomp (my @resources = <$res>);
639              
640 0 0 0       return $property && $value ? grep { /\s+$property:\s+$value/ } @resources : @resources;
  0            
641             }
642              
643             sub addResource {
644 0     0 1   my $self = shift;
645 0           my $zoneName = shift;
646 0           my $resource = shift;
647 0           my $props = shift;
648              
649 0           my @cmd = ($ZONECFG, '-z', $zoneName, 'add', "$resource;");
650              
651 0           for my $property (keys %$props) {
652             # check if it is an LX net property
653 0 0         if (grep { $_ eq $property } @LXNETPROPS) {
  0            
654 0           my ($prop, $value) = $self->$encodeLXnetProp($property, $props->{$property});
655 0           push @cmd, ('add', $prop, $value, ';');
656             }
657             else {
658 0           push @cmd, ('set', "$property=$props->{$property};");
659             }
660             }
661 0           push @cmd, qw(end);
662              
663 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
664 0 0         system(@cmd) and die "ERROR: cannot set properties for resource '$resource' of $zoneName\n";
665             }
666              
667             sub delResource {
668 0     0 1   my $self = shift;
669 0           my $zoneName = shift;
670 0           my $resource = shift;
671 0           my $property = shift;
672 0           my $value = shift;
673              
674 0 0         return if !$self->resourceExists($zoneName, $resource, $property, $value);
675              
676 0           my @cmd = ($ZONECFG, '-z', $zoneName, 'remove');
677 0 0 0       if ($property && $value) {
678 0           push @cmd, ($resource, $property, '=', $value);
679             }
680             else {
681 0           push @cmd, ('-F', $resource);
682             }
683            
684 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
685 0 0         system(@cmd) and die "ERROR: cannot remove resource '$resource' of $zoneName\n";
686             }
687              
688             sub clearResources {
689 0     0 1   my $self = shift;
690 0           my $zoneName = shift;
691              
692 0           for my $res (@{$RESOURCES->()}) {
  0            
693 0           $self->delResource($zoneName, $res);
694             }
695             }
696              
697             sub setProperty {
698 0     0 1   my $self = shift;
699 0           my $zoneName = shift;
700 0           my $property = shift;
701 0           my $value = shift;
702              
703 0           my @cmd = ($ZONECFG, '-z', $zoneName, 'set', $property, '=', "\"$value\"");
704              
705 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
706 0 0         system(@cmd) and die "ERROR: cannot set property $property of $zoneName\n";
707             }
708              
709             sub clearProperty {
710 0     0 1   my $self = shift;
711 0           my $zoneName = shift;
712 0           my $property = shift;
713              
714 0           my @cmd = ($ZONECFG, '-z', $zoneName, 'clear', $property);
715              
716 0 0         print STDERR '# ' . join(' ', @cmd) . "\n" if $self->{debug};
717 0 0         system(@cmd) and die "ERROR: cannot remove property $property of $zoneName\n";
718             }
719              
720             1;
721              
722             __END__