manage_profile.in 17 KB
Newer Older
1
2
#!/usr/bin/perl -w
#
3
# Copyright (c) 2000-2015 University of Utah and the Flux Group.
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
#
use English;
use strict;
use Getopt::Std;
use XML::Simple;
28
use File::Temp qw(tempfile :POSIX );
29
30
use Data::Dumper;
use CGI;
31
32
use POSIX ":sys_wait_h";
use POSIX qw(setsid);
33
34
35
36
37
38

#
# Back-end script to manage APT profiles.
#
sub usage()
{
39
40
41
42
    print("Usage: manage_profile create [-s uuid] <xmlfile>\n");
    print("Usage: manage_profile update <profile> <xmlfile>\n");
    print("Usage: manage_profile publish <profile>\n");
    print("Usage: manage_profile delete <profile>\n");
43
44
    exit(-1);
}
45
my $optlist     = "ds:t:";
46
my $debug       = 0;
47
my $update      = 0;
48
49
my $snap        = 0;
my $uuid;
50
my $rspec;
51
my $script;
52
my $profile;
53
my $instance;
54
55
my $aggregate;
my $node_id;
56
57
my $webtask;
my $webtask_id;
58
59
60
61

#
# Configure variables
#
62
63
my $TB		    = "@prefix@";
my $TBOPS           = "@TBOPSEMAIL@";
64
my $TBLOGS	    = "@TBLOGSEMAIL@";
65
my $MANAGEINSTANCE  = "$TB/bin/manage_instance";
66
my $RUNGENILIB      = "$TB/bin/rungenilib";
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

#
# Untaint the path
#
$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/usr/bin:/usr/sbin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

#
# Turn off line buffering on output
#
$| = 1;

#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use EmulabConstants;
use emdb;
use emutil;
86
use libEmulab;
87
88
89
use User;
use Project;
use APT_Profile;
90
91
92
use APT_Instance;
use GeniXML;
use GeniHRN;
93
use WebTask;
94
use EmulabFeatures;
95
96
97

# Protos
sub fatal($);
98
sub UserError(;$);
99
sub DeleteProfile($);
100
sub CanDelete($);
101
sub PublishProfile($);
102

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# Parse args below.
if (@ARGV < 2) {
    usage();
}
my $action = shift(@ARGV);

# The web interface (and in the future the xmlrpc interface) sets this.
my $this_user = User->ImpliedUser();
if (! defined($this_user)) {
    $this_user = User->ThisUser();
    if (!defined($this_user)) {
	fatal("You ($UID) do not exist!");
    }
}

if ($action eq "delete") {
    exit(DeleteProfile($ARGV[0]));
}
elsif ($action eq "publish") {
    exit(PublishProfile($ARGV[0]));
}
elsif (! ($action eq "create" || $action eq "update")) {
    usage();
}

128
129
130
131
132
133
134
135
136
137
138
#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
my %options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"d"})) {
    $debug = 1;
}
139
if (defined($options{"s"})) {
140
141
    $snap = 1;
    $uuid = $options{"s"};
142
}
143
144
145
if (defined($options{"t"})) {
    $webtask_id = $options{"t"};
}
146
147
148
if ($action eq "update") {
    $update = 1;
    $uuid = shift(@ARGV);
149
}
150
my $xmlfile = shift(@ARGV);
151
152
153
154
155
156
157

#
# These are the fields that we allow to come in from the XMLfile.
#
my $SLOT_OPTIONAL	= 0x1;	# The field is not required.
my $SLOT_REQUIRED	= 0x2;  # The field is required and must be non-null.
my $SLOT_ADMINONLY	= 0x4;  # Only admins can set this field.
158
my $SLOT_UPDATE 	= 0x8;  # Allowed to update.
159
160
161
162
163
164
165
166
167
#
# XXX We should encode all of this in the DB so that we can generate the
# forms on the fly, as well as this checking code.
#
my %xmlfields =
    # XML Field Name        DB slot name         Flags             Default
    ("profile_name"	   => ["name",		$SLOT_REQUIRED],
     "profile_pid"	   => ["pid",		$SLOT_REQUIRED],
     "profile_creator"	   => ["creator",	$SLOT_OPTIONAL],
168
     "profile_listed"      => ["listed",	$SLOT_OPTIONAL|$SLOT_UPDATE],
169
     "profile_public"      => ["public",	$SLOT_OPTIONAL|$SLOT_UPDATE],
Leigh B Stoller's avatar
Leigh B Stoller committed
170
     "profile_shared"      => ["shared",	$SLOT_OPTIONAL|$SLOT_UPDATE],
171
172
     "profile_topdog"      => ["topdog",	$SLOT_OPTIONAL|
			                          $SLOT_UPDATE|$SLOT_ADMINONLY],
173
     "rspec"		   => ["rspec",		$SLOT_REQUIRED|$SLOT_UPDATE],
174
     "script"		   => ["script",	$SLOT_OPTIONAL|$SLOT_UPDATE],
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
);

#
# Must wrap the parser in eval since it exits on error.
#
my $xmlparse = eval { XMLin($xmlfile,
			    VarAttr => 'name',
			    ContentKey => '-content',
			    SuppressEmpty => undef); };
fatal($@)
    if ($@);

#
# Process and dump the errors (formatted for the web interface).
# We should probably XML format the errors instead but not sure I want
# to go there yet.
#
my %errors = ();

#
# Make sure all the required arguments were provided.
#
my $key;
foreach $key (keys(%xmlfields)) {
    my (undef, $required, undef) = @{$xmlfields{$key}};

    $errors{$key} = "Required value not provided"
	if ($required & $SLOT_REQUIRED  &&
	    ! exists($xmlparse->{'attribute'}->{"$key"}));
}
UserError()
    if (keys(%errors));

#
# We build up an array of arguments to create.
#
my %new_args = ();
212
my %update_args = ();
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244

foreach $key (keys(%{ $xmlparse->{'attribute'} })) {
    my $value = $xmlparse->{'attribute'}->{"$key"}->{'value'};
    if (!defined($value)) {	# Empty string comes from XML as an undef value.
	$xmlparse->{'attribute'}->{"$key"}->{'value'} = $value = "";
    }

    print STDERR "User attribute: '$key' -> '$value'\n"
	if ($debug);

    my $field = $key;
    if (!exists($xmlfields{$field})) {
	next; # Skip it.
    }
    my ($dbslot, $required, $default) = @{$xmlfields{$field}};

    if ($required & $SLOT_REQUIRED) {
	# A slot that must be provided, so do not allow a null value.
	if (!defined($value)) {
	    $errors{$key} = "Must provide a non-null value";
	    next;
	}
    }
    if ($required & $SLOT_OPTIONAL) {
	# Optional slot. If value is null skip it. Might not be the correct
	# thing to do all the time?
	if (!defined($value)) {
	    next
		if (!defined($default));
	    $value = $default;
	}
    }
245
    if ($required & $SLOT_ADMINONLY) {
246
247
248
249
250
251
252
253
254
255
256
257
	# Admin implies optional, but thats probably not correct approach.
	$errors{$key} = "Administrators only"
	    if (! $this_user->IsAdmin());
    }
	
    # Now check that the value is legal.
    if (! TBcheck_dbslot($value, "apt_profiles",
			 $dbslot, TBDB_CHECKDBSLOT_ERROR)) {
	$errors{$key} = TBFieldErrorString();
	next;
    }
    $new_args{$dbslot} = $value;
258
259
    $update_args{$dbslot} = $value
	if ($update && ($required & $SLOT_UPDATE));
260
261
262
263

    if ($key eq "rspec") {
	$rspec = $value;
    }
264
265
266
    elsif ($key eq "script") {
	$script = $value;
    }
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
}
UserError()
    if (keys(%errors));

#
# We need to make sure the project exists and is a valid project for
# the creator (current user). 
#
my $project = Project->Lookup($new_args{"pid"});
if (!defined($project)) {
    $errors{"profile_pid"} = "No such project exists";
}
elsif (!$project->AccessCheck($this_user, TB_PROJECT_MAKEIMAGEID())) {
    $errors{"profile_pid"} = "Not enough permission in this project";
}

283
284
285
286
287
288
289
290
291
# Check datasets.
if (defined($rspec)) {
    my $errmsg = "Bad dataset";
    if (APT_Profile::CheckDatasets($rspec, $project->pid(), \$errmsg)) {
	$errors{"error"} = $errmsg;
	UserError();
    }
}

292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#
# See if this is a Parameterized Profile. Generate and store the form
# data if it is.
#
if (defined($script) && $script ne "") {
    my ($fh, $filename) = tempfile();
    fatal("Could not open temporary file for script")
	if (!defined($fh));
    print $fh $script;
    my $paramdefs = `$RUNGENILIB -p $filename`;
    fatal("$RUNGENILIB failed")
	if ($?);
    chomp($paramdefs);
    if ($paramdefs ne "") {
	if ($update) {
	    $update_args{"paramdefs"} = $paramdefs;
	}
	else {
	    $new_args{"paramdefs"} = $paramdefs;
	}
    }
}

315
316
317
318
#
# Are we going to snapshot a node in an experiment? If so we
# sanity check to make sure there is just one node. 
#
319
320
if ($snap) {
    $instance = APT_Instance->Lookup($uuid);
321
    if (!defined($instance)) {
322
	fatal("Could not look up instance $uuid");
323
    }
324
325
326
327
    if ($instance->status() ne "ready") {
	$errors{"error"} = "Instance must be in the ready state for cloning";
	UserError();
    }
328
    if ($instance->AggregateList() != 1) {
329
330
331
	$errors{"error"} = "Must be only one aggregate to snapshot";
	UserError();
    }
332
    ($aggregate) = $instance->AggregateList();
333
    my $manifest = GeniXML::Parse($aggregate->manifest());
334
335
336
337
338
339
340
341
    if (! defined($manifest)) {
	fatal("Could not parse manifest");
    }
    my @nodes = GeniXML::FindNodes("n:node", $manifest)->get_nodelist();
    if (@nodes != 1) {
	$errors{"error"} = "Too many nodes (> 1) to snapshot";
	UserError();
    }
342
    my $sliver_urn = GeniXML::GetSliverId($nodes[0]);
343
    my $manager_urn= GeniXML::GetManagerId($nodes[0]);
344
    $node_id       = GeniXML::GetVirtualId($nodes[0]);
345
346
    if (! (defined($sliver_urn) &&
	   $manager_urn eq $aggregate->aggregate_urn())) {
347
348
349
	$errors{"error"} = "$node_id is not at " . $aggregate->aggregate_urn();
	UserError();
    }
350
}
351
if ($update) {
352
    $profile = APT_Profile->Lookup($uuid);
353
    if (!defined($profile)) {
354
	fatal("Could not lookup profile for update $uuid");
355
    }
356
    # Kill the description.. No longer used.
357
358
    delete($update_args{"description"});

359
360
361
362
363
364
365
    #
    # Check for version feature.
    #
    my $doversions =
	EmulabFeatures->FeatureEnabled("APT_ProfileVersions",
				       $this_user, $project);

366
    #
367
    # If the rspec/script changed, then make a new version of the profile.
368
369
    # Everything else is metadata.
    #
370
371
372
373
374
    if (exists($update_args{"rspec"}) || exists($update_args{"script"})) {
	if ((exists($update_args{"rspec"}) &&
	     $update_args{"rspec"} ne $profile->rspec()) ||
	    (exists($update_args{"script"}) &&
	     $update_args{"script"} ne $profile->script())) {
375
	    if ($doversions) {
376
377
378
379
		$profile = $profile->NewVersion($this_user);
		if (!defined($profile)) {
		    fatal("Could not create new version of the profile");
		}
380
	    }
381
382
383
384
	    $profile->UpdateVersion({"rspec" => $update_args{"rspec"}})
		if (exists($update_args{"rspec"}));
	    $profile->UpdateVersion({"script" => $update_args{"script"}})
		if (exists($update_args{"script"}));
385
386
	    $profile->UpdateVersion({"paramdefs" => $update_args{"paramdefs"}})
		if (exists($update_args{"paramdefs"}));
387
	}
388
389
390
391
	delete($update_args{"rspec"})
	    if (exists($update_args{"rspec"}));
	delete($update_args{"script"})
	    if (exists($update_args{"script"}));
392
393
	delete($update_args{"paramdefs"})
	    if (exists($update_args{"paramdefs"}));
394
395
    }
    $profile->UpdateMetaData(\%update_args) == 0 or
396
	fatal("Could not update profile record");
397

398
399
    # Bump the modtime.
    $profile->MarkModified();
400
401
402
}
else {
    my $usererror;
403
404

    $profile = APT_Profile->Lookup($new_args{"pid"}, $new_args{"name"});
405
406
407
408
409
    if (defined($profile)) {
	$errors{"profile_name"} = "Already in use";
	UserError();
    }
    $profile =
Leigh B Stoller's avatar
Leigh B Stoller committed
410
	APT_Profile->Create($profile, $project,
411
			    $this_user, \%new_args, \$usererror);
412
413
414
415
416
417
418
    if (!defined($profile)) {
	if (defined($usererror)) {
	    $errors{"profile_name"} = $usererror;
	    UserError();
	}
	fatal("Could not create new profile");
    }
419
420
421
    if (!$this_user->IsAdmin()) {
	$profile->Publish();
    }
422
}
423
424
425
426
427
428
429

#
# Now do the snapshot operation.
#
if (defined($instance)) {
    my $apt_uuid   = $instance->uuid();
    my $imagename  = $profile->name();
Leigh B Stoller's avatar
Leigh B Stoller committed
430
    my $new_uuid   = $profile->uuid();
431

432
433
    #
    # Grab the webtask object so we can watch it. We are looking
434
435
436
437
    # for it to finish, so we can unlock the profile for use. Note
    # this always creates a webtask, even if not directed to on the
    # commmand line, so that we can communicate with the script we
    # call that does the work. 
438
439
440
    #
    $webtask = WebTask->Create($profile->uuid(), $webtask_id);
    if (!defined($webtask)) {
441
	$profile->Delete(1);
442
    }
443
    $webtask->AutoStore(1);
444
445

    if ($profile->Lock()) {
446
	$profile->Delete(1);
447
448
449
	fatal("Could not lock new profile");
    }

450
    my $command = "$MANAGEINSTANCE -t " . $webtask->task_id() . " -- ".
Leigh B Stoller's avatar
Leigh B Stoller committed
451
	"snapshot $apt_uuid -c $new_uuid -n $node_id -i $imagename";
452
    
453
454
    #
    # This returns pretty fast, and then the imaging takes place in
455
456
457
    # the background at the aggregate. The script keeps a process
    # running in the background waiting for the sliver to unlock and
    # the sliverstatus to indicate the node is running again.
458
459
460
    #
    my $output = emutil::ExecQuiet($command);
    if ($?) {
461
	$profile->Delete(1);
462
463
464
	$webtask->Delete()
	    if (!defined($webtask_id));
	print STDERR $output . "\n";
465
466
467
	fatal("Failed to create disk image!");
    }
    #
468
    # The script helpfully put the new image urn in the webtask.
469
    #
470
    $webtask->Refresh();
471
472
473
474
475
476
477
478
479
480
481
482
    my $newimage;

    if (GetSiteVar("protogeni/use_imagetracker") &&	
	EmulabFeatures->FeatureEnabled("APT_UseImageTracker",
				       $this_user, $project)) {
	$newimage = $webtask->image_urn();
    }
    else {
	$newimage = $webtask->image_url();
    }
    if (!defined($newimage) ||
	$profile->UpdateDiskImage($node_id, $newimage, 0)) {
483
484
	$webtask->Delete()
	    if (!defined($webtask_id));
485
486
	$profile->Delete(1);
	fatal("Could not update image URN in rspec");
487
    }
488
489
490
491
492
493
494
495
496

    #
    # Exit and leave child to poll.
    #
    if (! $debug) {
	my $child = fork();
	if ($child) {
	    exit(0);
	}
497
498
499
500
501
502
	# Close our descriptors so web server thinks we are disconnected.
	if ($webtask_id) {
	    for (my $i = 0; $i < 1024; $i++) {
	        POSIX::close($i);
	    }
	}
503
504
	# Let parent exit;
	sleep(2);
505
        POSIX::setsid();
506
    }
507
508
509
510
511
512
513
514
    #
    # We are waiting for the backend process to exit. The web interface is
    # reading the webtask structure, but if it fails we want to know that
    # so we can delete the profile. 
    #
    while (1) {
	sleep(10);
	
515
516
517
	$webtask->Refresh();
	last
	    if (defined($webtask->exited()));
518
519
520
521
522
523
524
525
526
527
528
529
530
531

	#
	# See if the process is still running. If not then it died badly.
	# Mark the webtask as exited.
	#
	my $pid = $webtask->process_id();
	if (! kill(0, $pid)) {
	    # Check again in case it just exited.
	    $webtask->Refresh();
	    if (! defined($webtask->exited())) {
		$webtask->Exited(-1);
	    }
	    last;
	}
532
533
    }
    if ($webtask->exitcode()) {
534
	$profile->Delete(1);
535
536
	$webtask->Delete()
	    if (!defined($webtask_id));
537
538
539
	exit(1);
    }
    $profile->Unlock();
540
541
    $webtask->Delete()
	if (!defined($webtask_id));
542
    exit(0);
543
}
544
545
546
547
548
549

my $portalLogs =
    ($project->isAPT() ? "aptnet-logs\@flux.utah.edu" :
     $project->isCloud() ? "cloudlab-logs\@flux.utah.edu" : $TBLOGS);

$project->SendEmail($portalLogs, "New Profile Created",
550
	 "Name:     ". $profile->versname() . "\n".
551
552
553
554
	 "User:     ". $profile->creator() . "\n".
	 "Project:  ". $profile->pid() .
	                 " (" . $project->Brand()->brand() . ")\n".
	 "UUID:     ". $profile->uuid() . "\n".
555
	 "URL:      ". $profile->AdminURL() . "\n");
556

557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
exit(0);

sub fatal($)
{
    my ($mesg) = @_;

    print STDERR "*** $0:\n".
	         "    $mesg\n";
    # Exit with negative status so web interface treats it as system error.
    exit(-1);
}

#
# Generate a simple XML file that PHP can parse. The web interface
# relies on using the same name attributes for the errors, as for
# the incoming values.
#
574
sub UserError(;$)
575
{
576
577
578
579
580
    my ($msg) = @_;
    
    if (defined($msg)) {
	$errors{"error"} = $msg;
    }
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
    if (keys(%errors)) {
	print "<errors>\n";
	foreach my $key (keys(%errors)) {
    	    print "<error name='$key'>" . CGI::escapeHTML($errors{$key});
	    print "</error>\n";
	}
	print "</errors>\n";
    }
    # Exit with positive status so web interface treats it as user error.
    exit(1);
}

sub escapeshellarg($)
{
    my ($str) = @_;

    $str =~ s/[^[:alnum:]]/\\$&/g;
    return $str;
}
600
601
602
603
604
605
606
607
608
609
610

#
# Delete a profile.
#
sub DeleteProfile($)
{
    my ($name)  = @_;
    my $profile = APT_Profile->Lookup($name);
    if (!defined($profile)) {
	fatal("No such profile exists");
    }
611
612
613
    if (!$profile->IsHead()) {
	UserError("Only allowed to delete the most recent profile");
    }
614
615
616
    if (!CanDelete($profile)) {
	UserError("Not allowed to delete this profile (version)");
    }
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
    #
    # Version zero is special of course.
    #
    if ($profile->version()) {
	$profile->DeleteVersion(0) == 0 or
	    fatal("Could not delete profile version");
    }
    else {
	# Purge it. At some point we want to save them.
	$profile->Delete(1) == 0 or
	    fatal("Could not delete profile");
    }
    return 0;
}

#
# Publish a profile.
#
sub PublishProfile($)
{
    my ($name)  = @_;
    my $profile = APT_Profile->Lookup($name);
    if (!defined($profile)) {
	fatal("No such profile exists");
    }
    if (!$profile->IsHead()) {
	UserError("Only allowed to publish the most recent profile");
    }
    $profile->Publish() == 0 or
	fatal("Could not publish profile");
647
648
    return 0;
}
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672

#
#
#
sub CanDelete($)
{
    my ($profile) = @_;
    
    # Want to know if the project is APT or Cloud/Emulab. APT projects
    # may not delete profiles (yet).
    my $project = Project->Lookup($profile->pid_idx());
    return 0
	if (!defined($project));
    return 0
        if (!$profile->IsHead());
    return 1
	if ($this_user->IsAdmin() || $this_user->stud());
    return 1
        if (!$project->isAPT());
    # APT profiles may not be deleted if published.
    return 1
        if (!$profile->published());
    return 0;
}