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