xref: /titanic_50/usr/src/cmd/projadd/projmod.pl (revision c77a61a72b5ecdc507d6cf104142edd371a16c84)
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, Version 1.0 only
7# (the "License").  You may not use this file except in compliance
8# with the License.
9#
10# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
11# or http://www.opensolaris.org/os/licensing.
12# See the License for the specific language governing permissions
13# and limitations under the License.
14#
15# When distributing Covered Code, include this CDDL HEADER in each
16# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
17# If applicable, add the following below this CDDL HEADER, with the
18# fields enclosed by brackets "[]" replaced with your own identifying
19# information: Portions Copyright [yyyy] [name of copyright owner]
20#
21# CDDL HEADER END
22#
23#
24# Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
25# Use is subject to license terms.
26#
27#ident	"%Z%%M%	%I%	%E% SMI"
28#
29
30require 5.005;
31use strict;
32use locale;
33use Errno;
34use Fcntl;
35use File::Basename;
36use Getopt::Std;
37use Getopt::Long qw(:config no_ignore_case bundling);
38use POSIX qw(locale_h);
39use Sun::Solaris::Utils qw(textdomain gettext);
40use Sun::Solaris::Project qw(:ALL :PRIVATE);
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] [-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 = &PROJF_PATH;
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;
375
376GetOptions("f=s" => \$projfile,
377	   "n"   => \$opt_n,
378	   "c=s" => \$opt_c,
379	   "o"	 => \$opt_o,
380	   "p=s" => \$opt_p,
381	   "l=s" => \$opt_l,
382	   "s"	 => \$opt_s,
383	   "r"	 => \$opt_r,
384	   "a"	 => \$opt_a,
385	   "U=s" => \$opt_U,
386	   "G=s" => \$opt_G,
387	   "K=s" => \@opt_K) || usage();
388
389usage(gettext('Invalid command-line arguments')) if (@ARGV > 1);
390
391if ($opt_c || $opt_G || $opt_l || $opt_p || $opt_U || @opt_K) {
392	$modify = 1;
393	if (! defined($ARGV[0])) {
394		usage(gettext('No project name specified'));
395	}
396}
397
398if (!$modify && defined($ARGV[0])) {
399	usage(gettext('missing -c, -G, -l, -p, -U, or -K'));
400}
401
402if ($modify && $projfile eq '-') {
403	usage(gettext('Cannot modify standard input'));
404}
405
406$pname = $ARGV[0];
407usage(gettext('-o requires -p projid to be specified'))
408    if (defined($opt_o) && ! defined($opt_p));
409usage(gettext('-a, -r, and -s are mutually exclusive'))
410    if ((defined($opt_a) && (defined($opt_r) || defined($opt_s))) ||
411	(defined($opt_r) && (defined($opt_a) || defined($opt_s))) ||
412	(defined($opt_s) && (defined($opt_a) || defined($opt_r))));
413
414usage(gettext('-a and -r require -U users or -G groups to be specified'))
415    if ((defined($opt_a) || defined($opt_r) || defined($opt_s)) &&
416    ! (defined($opt_U) || defined($opt_G) || (@opt_K)));
417
418
419if (defined($opt_a)) {
420	$flags->{mode} = 'add';
421} elsif (defined($opt_r)) {
422	$flags->{mode} = 'remove';
423} elsif (defined($opt_s)) {
424	$flags->{mode} = 'substitute';
425} else {
426	$flags->{mode} = 'replace';
427}
428
429# Fabricate an unique temporary filename.
430my $tmpprojf = $projfile . ".tmp.$$";
431
432my $pfh;
433
434#
435# Read the project file.  sysopen() is used so we can control the file mode.
436# Handle special case for standard input.
437if ($projfile eq '-') {
438	open($pfh, "<&=STDIN") or error( [10,
439	    gettext('Cannot open standard input')]);
440} elsif (! sysopen($pfh, $projfile, O_RDONLY)) {
441	error([10, gettext('Cannot open %s: %s'), $projfile, $!]);
442}
443my ($mode, $uid, $gid) = (stat($pfh))[2,4,5];
444
445
446if ($opt_n) {
447	$flags->{'validate'} = 'false';
448} else {
449	$flags->{'validate'} = 'true';
450}
451
452$flags->{'res'} = 'true';
453$flags->{'dup'} = 'true';
454
455my ($ret, $pf) = projf_read($pfh, $flags);
456if ($ret != 0) {
457	error(@$pf);
458}
459close($pfh);
460my $err;
461my $tmperr;
462my $value;
463
464# Find existing record.
465my ($proj, $idx);
466$idx = 0;
467
468if (defined($pname)) {
469	foreach my $r (@$pf) {
470		if ($r->{'name'} eq $pname) {
471			$proj = $r;
472			last;
473		}
474		$idx++;
475	}
476	error([6, gettext('Project "%s" does not exist'), $pname])
477	    if (! $proj);
478}
479#
480# If there are no modification options, simply reading the file, which
481# includes parsing and verifying, is sufficient.
482#
483if (!$modify) {
484	exit(0);
485}
486
487foreach my $r (@$pf) {
488	if ($r->{'name'} eq $pname) {
489		$proj = $r;
490		last;
491	}
492	$idx++;
493}
494
495# Update the record as appropriate.
496$err = [];
497
498# Set new project name.
499if (defined($opt_l)) {
500
501	($ret, $value) = projent_parse_name($opt_l);
502	if ($ret != 0) {
503		push(@$err, @$value);
504	} else {
505		$proj->{'name'} = $value;
506		if (!defined($opt_n)) {
507			($ret, $tmperr) =
508			    projent_validate_unique_name($proj, $pf);
509			if ($ret != 0) {
510				push(@$err, @$tmperr);
511			}
512		}
513	}
514}
515
516# Set new project id.
517if (defined($opt_p)) {
518
519	($ret, $value) = projent_parse_projid($opt_p);
520	if ($ret != 0) {
521		push(@$err, @$value);
522	} else {
523		$proj->{'projid'} = $value;
524
525		# Check for dupicate.
526		if ((!defined($opt_n)) && (!defined($opt_o))) {
527			($ret, $tmperr) =
528			    projent_validate_unique_id($proj, $pf);
529			if ($ret != 0) {
530				push(@$err, @$tmperr);
531			}
532		}
533	}
534}
535
536# Set new comment.
537if (defined($opt_c)) {
538
539	($ret, $value) = projent_parse_comment($opt_c);
540	if ($ret != 0) {
541		push(@$err, @$value);
542	} else {
543		$proj->{'comment'} = $value;
544	}
545}
546
547# Set new users.
548if (defined($opt_U)) {
549
550	my @sortlist;
551	my $list;
552	($ret, $list) = projent_parse_users($opt_U, {'allowspaces' => 1});
553	if ($ret != 0) {
554		push(@$err, @$list);
555	} else {
556		($ret, $list) =
557		    merge_lists($list, $proj->{'userlist'}, $flags->{mode});
558		if ($ret != 0) {
559			push(@$err, @$list);
560		} else {
561			@sortlist = sort(@$list);
562			$proj->{'userlist'} = \@sortlist;
563		}
564	}
565}
566
567# Set new groups.
568if (defined($opt_G)) {
569
570	my @sortlist;
571	my $list;
572	($ret, $list) = projent_parse_groups($opt_G, {'allowspaces' => 1});
573	if ($ret != 0) {
574		push(@$err, @$list);
575	} else {
576		($ret, $list) =
577		    merge_lists($list, $proj->{'grouplist'}, $flags->{mode});
578		if ($ret != 0) {
579			push(@$err, @$list);
580		} else {
581			@sortlist = sort(@$list);
582			$proj->{'grouplist'} = \@sortlist;
583		}
584	}
585}
586
587# Set new attributes.
588my $attrib;
589my @attriblist;
590
591foreach $attrib (@opt_K) {
592
593	my $list;
594	($ret, $list) = projent_parse_attributes($attrib, {'allowunits' => 1});
595	if ($ret != 0) {
596		push(@$err, @$list);
597	} else {
598		push(@attriblist, @$list);
599	}
600}
601
602if (@attriblist) {
603	my @sortlist;
604	my $list;
605
606	($ret, $list) =
607	    merge_attribs(\@attriblist, $proj->{'attributelist'},
608	    $flags->{mode});
609	if ($ret != 0) {
610		push(@$err, @$list);
611	} else {
612		@sortlist =
613		    sort { $a->{'name'} cmp $b->{'name'} } @$list;
614		$proj->{'attributelist'} = \@sortlist;
615	}
616}
617
618# Validate all projent fields.
619if (!defined($opt_n)) {
620	($ret, $tmperr) = projent_validate($proj, $flags);
621	if ($ret != 0) {
622		push(@$err, @$tmperr);
623	}
624}
625if (@$err) {
626	error(@$err);
627}
628
629# Write out the project file.
630if ($modify) {
631
632	#
633	# Mark projent to write based on new values instead of
634	# original line.
635	#
636	$proj->{'modified'} = 'true';
637	umask(0000);
638	sysopen($pfh, $tmpprojf, O_WRONLY | O_CREAT | O_EXCL, $mode) ||
639	    error([10, gettext('Cannot create %s: %s'), $tmpprojf, $!]);
640	projf_write($pfh, $pf);
641	close($pfh);
642
643	# Update file attributes.
644	if (!chown($uid, $gid, $tmpprojf)) {
645		unlink($tmpprojf);
646		error([10, gettext('Cannot set ownership of %s: %s'),
647		    $tmpprojf, $!]);
648	}
649	if (! rename($tmpprojf, $projfile)) {
650		unlink($tmpprojf);
651		error([10, gettext('cannot rename %s to %s: %s'),
652	            $tmpprojf, $projfile, $!]);
653	}
654
655}
656
657exit(0);
658
659
660
661
662