xref: /illumos-gate/usr/src/cmd/projadd/projmod.pl (revision 2bbdd445a21f9d61f4a0ca0faf05d5ceb2bd91f3)
1#!/usr/perl5/bin/perl -w
2#
3# CDDL HEADER START
4#
5# The contents of this file are subject to the terms of the
6# Common Development and Distribution License (the "License").
7# You may not use this file except in compliance with the License.
8#
9# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10# or http://www.opensolaris.org/os/licensing.
11# See the License for the specific language governing permissions
12# and limitations under the License.
13#
14# When distributing Covered Code, include this CDDL HEADER in each
15# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16# If applicable, add the following below this CDDL HEADER, with the
17# fields enclosed by brackets "[]" replaced with your own identifying
18# information: Portions Copyright [yyyy] [name of copyright owner]
19#
20# CDDL HEADER END
21#
22#
23# Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
24# Use is subject to license terms.
25#
26#ident	"%Z%%M%	%I%	%E% SMI"
27#
28
29require 5.005;
30use strict;
31use locale;
32use Errno;
33use Fcntl;
34use File::Basename;
35use Getopt::Std;
36use Getopt::Long qw(:config no_ignore_case bundling);
37use POSIX qw(locale_h);
38use Sun::Solaris::Utils qw(textdomain gettext);
39use Sun::Solaris::Project qw(:ALL :PRIVATE);
40use Sun::Solaris::Task qw(:ALL);
41
42#
43# Print a usage message and exit.
44#
45sub usage
46{
47	my (@msg) = @_;
48	my $prog = basename($0);
49	my $space = ' ' x length($prog);
50	print(STDERR "$prog: @msg\n") if (@msg);
51	printf(STDERR gettext(
52	    "Usage: %s [-n] [-f filename]\n"), $prog);
53	printf(STDERR gettext(
54	    "       %s [-n] [-A|-f filename] [-p projid [-o]] [-c comment]\n".
55            "       %s [-a|-s|-r] [-U user[,user...]] [-G group[,group...]]\n".
56            "       %s [-K name[=value[,value...]]] [-l new_projectname] ".
57	    "project\n"), $prog, $space, $space, $space);
58	exit(2);
59}
60
61#
62# Print a list of error messages and exit.
63#
64sub error
65{
66	my $exit = $_[0][0];
67	my $prog = basename($0) . ': ';
68	foreach my $err (@_) {
69		my ($e, $fmt, @args) = @$err;
70		printf(STDERR $prog . $fmt . "\n", @args);
71	}
72	exit($exit);
73}
74
75#
76# Merge an array of users/groups with an existing array.  The array to merge
77# is the first argument, an array ref is the second argument.  The third
78# argument is the mode which can be one of:
79#     add	add all entries in the first arg to the second
80#     remove	remove all entries in the first arg from the second
81#     replace	replace the second arg by the first
82# The resulting array is returned as a reference.
83#
84sub merge_lists
85{
86	my ($new, $old, $mode) = @_;
87	my @err;
88
89	if ($mode eq 'add') {
90		my @merged = @$old;
91		my %look = map { $_ => 1 } @$old;
92		my @leftover;
93		foreach my $e (@$new) {
94			if (! exists($look{$e})) {
95				push(@merged, $e);
96			} else {
97				push(@leftover, $e);
98			}
99		}
100		if (@leftover) {
101			push(@err,
102			    [6, gettext('Project already contains "%s"'),
103			    join(',', @leftover)]);
104			return (1, \@err);
105		}
106
107		return(0, \@merged);
108
109	} elsif ($mode eq 'remove') {
110
111		my %seen;
112		my @dups = grep($seen{$_}++ == 1, @$new);
113		if (@dups) {
114			push(@err, [6, gettext('Duplicate names "%s"'),
115			    join(',', @dups)]);
116			return (1, \@err);
117		}
118		my @merged;
119		my %look = map { $_ => 0 } @$new;
120		foreach my $e (@$old) {
121			if (exists($look{$e})) {
122				$look{$e}++;
123			} else {
124				push(@merged, $e);
125			}
126		}
127		my @leftover = grep(! $look{$_}, keys(%look));
128		if (@leftover) {
129			push(@err, [6,
130		            gettext('Project does not contain "%s"'),
131			    join(',', @leftover)]);
132			return (1, \@err);
133		}
134		return (0, \@merged);
135
136	} elsif ($mode eq 'replace' || $mode eq 'substitute') {
137		return (0, $new);
138	}
139}
140
141#
142# merge_values(ref to listA, ref to listB, mode
143#
144# Merges the values in listB with the values in listA.  Dups are not
145# merged away, but instead are maintained.
146#
147# modes:
148#	add   :	add values in listB to listA
149#	remove:	removes first instance of each value in listB from listA
150#
151sub merge_values
152{
153
154	my ($new, $old, $mode) = @_;
155	my $undefined;
156	my @merged;
157	my $lastmerged;
158	my ($oldval, $newval);
159	my $found;
160	my @err;
161
162	if (!defined($old) && !defined($new)) {
163		return (0, $undefined);
164	}
165
166	if ($mode eq 'add') {
167
168		if (defined($old)) {
169			push(@merged, @$old);
170		}
171		if (defined($new)) {
172			push(@merged, @$new);
173		}
174		return (0, \@merged);
175
176	} elsif ($mode eq 'remove') {
177
178		$lastmerged = $old;
179		foreach $newval (@$new) {
180			$found = 0;
181			@merged = ();
182			foreach $oldval (@$lastmerged) {
183				if (!$found &&
184				    projent_values_equal($newval, $oldval)) {
185					$found = 1;
186				} else {
187					push(@merged, $oldval);
188				}
189
190			}
191			if (!$found) {
192				push(@err, [6, gettext(
193				    'Value "%s" not found'),
194				    projent_values2string($newval)]);
195			}
196			@$lastmerged = @merged;
197		}
198
199		if (@err) {
200			return (1, \@err);
201		} else {
202			return (0, \@merged);
203		}
204	}
205}
206
207#
208# merge_attribs(listA ref, listB ref, mode)
209#
210# Merge listB of attribute/values hash refs with listA
211# Each hash ref should have keys "name" and "values"
212#
213# modes:
214#     add	For each attribute in listB, add its values to
215#	        the matching attribute in listA.  If listA does not
216#		contain this attribute, add it.
217#
218#     remove	For each attribute in listB, remove its values from
219#	        the matching attribute in listA.  If all of an
220#		attributes values are removed, the attribute is removed.
221#		If the attribute in listB has no values, then the attribute
222#		and all of it's values are removed from listA
223#
224#     substitute For each attribute in listB, replace the values of
225#	        the matching attribute in listA with its values.  If
226#		listA does not contain this attribute, add it.
227#
228#     replace	Return listB
229#
230# The resulting array is returned as a reference.
231#
232sub merge_attribs
233{
234	my ($new, $old, $mode) = @_;
235	my @merged;
236	my @err;
237	my $ret;
238	my $tmp;
239	my $newattrib;
240	my $oldattrib;
241	my $values;
242
243	if ($mode eq 'add') {
244
245		my %oldhash;
246		push(@merged, @$old);
247		%oldhash = map { $_->{'name'} => $_ } @$old;
248		foreach $newattrib (@$new) {
249
250			$oldattrib = $oldhash{$newattrib->{'name'}};
251			if (defined($oldattrib)) {
252				($ret, $tmp) = merge_values(
253				    $newattrib->{'values'},
254				    $oldattrib->{'values'},
255				    $mode);
256
257				if ($ret != 0) {
258					push(@err, @$tmp);
259				} else {
260					$oldattrib->{'values'} = $tmp;
261				}
262			} else {
263				push(@merged, $newattrib);
264			}
265		}
266		if (@err) {
267			return (1, \@err);
268		} else {
269			return (0, \@merged);
270		}
271
272	} elsif ($mode eq 'remove') {
273
274		my %seen;
275		my @dups = grep($seen{$_}++ == 1, map { $_->{'name'} } @$new);
276		if (@dups) {
277			push(@err, [6, gettext(
278			    'Duplicate Attributes "%s"'),
279			     join(',', @dups)]);
280			return (1, \@err);
281		}
282		my %toremove = map { $_->{'name'} => $_ } @$new;
283
284		foreach $oldattrib (@$old) {
285			$newattrib = $toremove{$oldattrib->{'name'}};
286			if (!defined($newattrib)) {
287
288				push(@merged, $oldattrib);
289
290			} else {
291				if (defined($newattrib->{'values'})) {
292					($ret, $tmp) = merge_values(
293					    $newattrib->{'values'},
294					    $oldattrib->{'values'},
295					    $mode);
296
297					if ($ret != 0) {
298						push(@err, @$tmp);
299					} else {
300						$oldattrib->{'values'} = $tmp;
301					}
302					if (defined($tmp) && @$tmp) {
303						push(@merged, $oldattrib);
304					}
305				}
306				delete $toremove{$oldattrib->{'name'}};
307			}
308		}
309		foreach $tmp (keys(%toremove)) {
310			push(@err, [6,
311		            gettext('Project does not contain "%s"'),
312			    $tmp]);
313		}
314
315		if (@err) {
316			return (1, \@err);
317		} else {
318			return (0, \@merged);
319		}
320
321	} elsif ($mode eq 'substitute') {
322
323		my %oldhash;
324		push(@merged, @$old);
325		%oldhash = map { $_->{'name'} => $_ } @$old;
326		foreach $newattrib (@$new) {
327
328			$oldattrib = $oldhash{$newattrib->{'name'}};
329			if (defined($oldattrib)) {
330
331				$oldattrib->{'values'} =
332				    $newattrib->{'values'};
333
334			} else {
335				push(@merged, $newattrib);
336			}
337		}
338		if (@err) {
339			return (1, \@err);
340		} else {
341			return (0, \@merged);
342		}
343
344	} elsif ($mode eq 'replace') {
345		return (0, $new);
346	}
347}
348
349#
350# Main routine of script.
351#
352# Set the message locale.
353#
354setlocale(LC_ALL, '');
355textdomain(TEXT_DOMAIN);
356
357
358# Process command options and do some initial command-line validity checking.
359my ($pname, $flags);
360$flags = {};
361my $modify = 0;
362
363my $projfile;
364my $opt_n;
365my $opt_c;
366my $opt_o;
367my $opt_p;
368my $opt_l;
369my $opt_a;
370my $opt_r;
371my $opt_s;
372my $opt_U;
373my $opt_G;
374my @opt_K;
375my $opt_A;
376
377GetOptions("f=s" => \$projfile,
378	   "n"   => \$opt_n,
379	   "c=s" => \$opt_c,
380	   "o"	 => \$opt_o,
381	   "p=s" => \$opt_p,
382	   "l=s" => \$opt_l,
383	   "s"	 => \$opt_s,
384	   "r"	 => \$opt_r,
385	   "a"	 => \$opt_a,
386	   "U=s" => \$opt_U,
387	   "G=s" => \$opt_G,
388	   "K=s" => \@opt_K,
389  	   "A"	 => \$opt_A) || usage();
390
391usage(gettext('Invalid command-line arguments')) if (@ARGV > 1);
392
393if ($opt_c || $opt_G || $opt_l || $opt_p || $opt_U || @opt_K || $opt_A) {
394	$modify = 1;
395	if (! defined($ARGV[0])) {
396		usage(gettext('No project name specified'));
397	}
398}
399
400if (!$modify && defined($ARGV[0])) {
401	usage(gettext('missing -c, -G, -l, -p, -U, or -K'));
402}
403
404if (defined($opt_A) && defined($projfile)) {
405	usage(gettext('-A and -f are mutually exclusive'));
406}
407
408if (! defined($projfile)) {
409	$projfile = &PROJF_PATH;
410}
411
412if ($modify && $projfile eq '-') {
413	usage(gettext('Cannot modify standard input'));
414}
415
416$pname = $ARGV[0];
417usage(gettext('-o requires -p projid to be specified'))
418    if (defined($opt_o) && ! defined($opt_p));
419usage(gettext('-a, -r, and -s are mutually exclusive'))
420    if ((defined($opt_a) && (defined($opt_r) || defined($opt_s))) ||
421	(defined($opt_r) && (defined($opt_a) || defined($opt_s))) ||
422	(defined($opt_s) && (defined($opt_a) || defined($opt_r))));
423
424usage(gettext('-a and -r require -U users or -G groups to be specified'))
425    if ((defined($opt_a) || defined($opt_r) || defined($opt_s)) &&
426    ! (defined($opt_U) || defined($opt_G) || (@opt_K)));
427
428
429if (defined($opt_a)) {
430	$flags->{mode} = 'add';
431} elsif (defined($opt_r)) {
432	$flags->{mode} = 'remove';
433} elsif (defined($opt_s)) {
434	$flags->{mode} = 'substitute';
435} else {
436	$flags->{mode} = 'replace';
437}
438
439# Fabricate an unique temporary filename.
440my $tmpprojf = $projfile . ".tmp.$$";
441
442my $pfh;
443
444#
445# Read the project file.  sysopen() is used so we can control the file mode.
446# Handle special case for standard input.
447if ($projfile eq '-') {
448	open($pfh, "<&=STDIN") or error( [10,
449	    gettext('Cannot open standard input')]);
450} elsif (! sysopen($pfh, $projfile, O_RDONLY)) {
451	error([10, gettext('Cannot open %s: %s'), $projfile, $!]);
452}
453my ($mode, $uid, $gid) = (stat($pfh))[2,4,5];
454
455
456if ($opt_n) {
457	$flags->{'validate'} = 'false';
458} else {
459	$flags->{'validate'} = 'true';
460}
461
462$flags->{'res'} = 'true';
463$flags->{'dup'} = 'true';
464
465my ($ret, $pf) = projf_read($pfh, $flags);
466if ($ret != 0) {
467	error(@$pf);
468}
469close($pfh);
470my $err;
471my $tmperr;
472my $value;
473
474# Find existing record.
475my ($proj, $idx);
476$idx = 0;
477
478if (defined($pname)) {
479	foreach my $r (@$pf) {
480		if ($r->{'name'} eq $pname) {
481			$proj = $r;
482			last;
483		}
484		$idx++;
485	}
486	error([6, gettext('Project "%s" does not exist'), $pname])
487	    if (! $proj);
488}
489#
490# If there are no modification options, simply reading the file, which
491# includes parsing and verifying, is sufficient.
492#
493if (!$modify) {
494	exit(0);
495}
496
497foreach my $r (@$pf) {
498	if ($r->{'name'} eq $pname) {
499		$proj = $r;
500		last;
501	}
502	$idx++;
503}
504
505# Update the record as appropriate.
506$err = [];
507
508# Set new project name.
509if (defined($opt_l)) {
510
511	($ret, $value) = projent_parse_name($opt_l);
512	if ($ret != 0) {
513		push(@$err, @$value);
514	} else {
515		$proj->{'name'} = $value;
516		if (!defined($opt_n)) {
517			($ret, $tmperr) =
518			    projent_validate_unique_name($proj, $pf);
519			if ($ret != 0) {
520				push(@$err, @$tmperr);
521			}
522		}
523	}
524}
525
526# Set new project id.
527if (defined($opt_p)) {
528
529	($ret, $value) = projent_parse_projid($opt_p);
530	if ($ret != 0) {
531		push(@$err, @$value);
532	} else {
533		$proj->{'projid'} = $value;
534
535		# Check for dupicate.
536		if ((!defined($opt_n)) && (!defined($opt_o))) {
537			($ret, $tmperr) =
538			    projent_validate_unique_id($proj, $pf);
539			if ($ret != 0) {
540				push(@$err, @$tmperr);
541			}
542		}
543	}
544}
545
546# Set new comment.
547if (defined($opt_c)) {
548
549	($ret, $value) = projent_parse_comment($opt_c);
550	if ($ret != 0) {
551		push(@$err, @$value);
552	} else {
553		$proj->{'comment'} = $value;
554	}
555}
556
557# Set new users.
558if (defined($opt_U)) {
559
560	my @sortlist;
561	my $list;
562	($ret, $list) = projent_parse_users($opt_U, {'allowspaces' => 1});
563	if ($ret != 0) {
564		push(@$err, @$list);
565	} else {
566		($ret, $list) =
567		    merge_lists($list, $proj->{'userlist'}, $flags->{mode});
568		if ($ret != 0) {
569			push(@$err, @$list);
570		} else {
571			@sortlist = sort(@$list);
572			$proj->{'userlist'} = \@sortlist;
573		}
574	}
575}
576
577# Set new groups.
578if (defined($opt_G)) {
579
580	my @sortlist;
581	my $list;
582	($ret, $list) = projent_parse_groups($opt_G, {'allowspaces' => 1});
583	if ($ret != 0) {
584		push(@$err, @$list);
585	} else {
586		($ret, $list) =
587		    merge_lists($list, $proj->{'grouplist'}, $flags->{mode});
588		if ($ret != 0) {
589			push(@$err, @$list);
590		} else {
591			@sortlist = sort(@$list);
592			$proj->{'grouplist'} = \@sortlist;
593		}
594	}
595}
596
597# Set new attributes.
598my $attrib;
599my @attriblist;
600
601foreach $attrib (@opt_K) {
602
603	my $list;
604	($ret, $list) = projent_parse_attributes($attrib, {'allowunits' => 1});
605	if ($ret != 0) {
606		push(@$err, @$list);
607	} else {
608		push(@attriblist, @$list);
609	}
610}
611
612if (@attriblist) {
613	my @sortlist;
614	my $list;
615
616	($ret, $list) =
617	    merge_attribs(\@attriblist, $proj->{'attributelist'},
618	    $flags->{mode});
619	if ($ret != 0) {
620		push(@$err, @$list);
621	} else {
622		@sortlist =
623		    sort { $a->{'name'} cmp $b->{'name'} } @$list;
624		$proj->{'attributelist'} = \@sortlist;
625	}
626}
627
628# Validate all projent fields.
629if (!defined($opt_n)) {
630	($ret, $tmperr) = projent_validate($proj, $flags);
631	if ($ret != 0) {
632		push(@$err, @$tmperr);
633	}
634}
635if (@$err) {
636	error(@$err);
637}
638
639# Write out the project file.
640if ($modify) {
641
642	#
643	# Mark projent to write based on new values instead of
644	# original line.
645	#
646	$proj->{'modified'} = 'true';
647	umask(0000);
648	sysopen($pfh, $tmpprojf, O_WRONLY | O_CREAT | O_EXCL, $mode) ||
649	    error([10, gettext('Cannot create %s: %s'), $tmpprojf, $!]);
650	projf_write($pfh, $pf);
651	close($pfh);
652
653	# Update file attributes.
654	if (!chown($uid, $gid, $tmpprojf)) {
655		unlink($tmpprojf);
656		error([10, gettext('Cannot set ownership of %s: %s'),
657		    $tmpprojf, $!]);
658	}
659	if (! rename($tmpprojf, $projfile)) {
660		unlink($tmpprojf);
661		error([10, gettext('cannot rename %s to %s: %s'),
662	            $tmpprojf, $projfile, $!]);
663	}
664
665}
666
667if (defined($opt_A)) {
668	my $error;
669
670	if (($error = setproject($pname, "root", TASK_FINAL|TASK_PROJ_PURGE)) != 0) {
671
672		if ($error == SETPROJ_ERR_TASK) {
673			if ($!{EAGAIN}) {
674				error([5, gettext("resource control limit has ".
675				     "been reached\n")]);
676			} elsif ($!{ESRCH}) {
677				error([5, gettext("user \"%s\" is not a member ".
678				     "of project \"%s\"\n"), "root", $pname]);
679			} elsif ($!{EACCES}) {
680				error([5, gettext("the invoking task is final\n"
681				     )]);
682			} else {
683				error([5, gettext("could not join project \"%s".
684				     "\"\n"), $pname]);
685			}
686
687		} elsif ($error == SETPROJ_ERR_POOL) {
688			if ($!{EACCES}) {
689				error([5, gettext("no resource pool accepting ".
690				     "default bindings exists for project \"%s".
691				     "\"\n"), $pname]);
692	        	} elsif ($!{ESRCH}) {
693				error([5, gettext("specified resource pool ".
694				     "does not exist for project \"%s\"\n"),
695				     $pname]);
696			} else {
697				error([5, gettext("could not bind to default ".
698				     "resource pool for project \"%s\"\n"),
699				     $pname]);
700			}
701
702		} else {
703			#
704			# $error represents the position - within the semi-colon
705			# delimited $attribute - that generated the error
706			#
707			if ($error <= 0) {
708				error([5, gettext("setproject failed for ".
709				     "project \"%s\"\n"), $pname]);
710			} else {
711				my ($name, $projid, $comment, $users_ref,
712				     $groups_ref, $attr) = getprojbyname($pname);
713				my $attribute = ($attr =~
714				     /(\S+?)=\S+?(?:;|\z)/g)[$error - 1];
715
716				if (!$attribute) {
717					error([5, gettext("warning, resource ".
718					     "control assignment failed for ".
719					     "project \"%s\" attribute %d\n"),
720					     $pname, $error]);
721				} else {
722					error([5, gettext("warning, %s ".
723					     "resource control assignment ".
724					     "failed for project \"%s\"\n"),
725					     $attribute, $pname]);
726				}
727			}
728		}
729	}
730}
731
732exit(0);
733