manage_dataset.in 34.1 KB
Newer Older
1
2
#!/usr/bin/perl -w
#
3
# Copyright (c) 2000-2017 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
28
29
30
31
32
33
34
35
36
37
38
39
# 
# {{{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 Data::Dumper;
use POSIX ":sys_wait_h";
use POSIX qw(setsid close);
use Date::Parse;

#
# Back-end script to manage APT profiles.
#
sub usage()
{
    print STDERR "Usage: manage_dataset [options --] create ...\n";
    print STDERR "Usage: manage_dataset [options --] delete ...\n";
    print STDERR "Usage: manage_dataset [options --] refresh ...\n";
40
41
    print STDERR "Usage: manage_dataset [options --] modify ...\n";
    print STDERR "Usage: manage_dataset [options --] extend ...\n";
42
    print STDERR "Usage: manage_dataset [options --] approve ...\n";
43
    print STDERR "Usage: manage_dataset [options --] snapshot ...\n";
44
    print STDERR "Usage: manage_dataset [options --] getcredential ...\n";
45
46
47
48
49
50
    exit(-1);
}
my $optlist     = "dt:";
my $debug       = 0;
my $webtask_id;
my $webtask;
51
52
my $this_user;
my $geniuser;
53
54
55
56
57
58

#
# Configure variables
#
my $TB		= "@prefix@";
my $TBOPS       = "@TBOPSEMAIL@";
59
my $SACERT      = "$TB/etc/genisa.pem";
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

#
# 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 libtestbed;
use EmulabConstants;
use emdb;
use emutil;
80
use libEmulab;
81
82
83
use User;
use Project;
use APT_Dataset;
84
use APT_Instance;
85
86
87
use WebTask;
use Blockstore;
use GeniResponse;
88
use GeniXML;
89
use GeniUser;
90
91
92
93
use GeniAuthority;
use GeniCertificate;
use GeniCredential;
use GeniImage;
94
95
96

# Protos
sub fatal($);
97
sub uerror($;$);
98
99
100
101
sub DoCreate();
sub DoDelete();
sub DoRefresh();
sub DoRefreshInternal($$);
102
sub DoGetCredential();
103
104
sub DoModify();
sub DoExtend();
105
sub DoApprove();
106
sub DoSnapshot();
107
sub DoSnapShotInternal($$$$$);
108
sub PollDatasetStatus($$$);
109
sub DoImageTrackerStuff($$$$$);
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
my %options = ();
if (grep {$_ eq "--"} @ARGV &&
    ! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"d"})) {
    $debug = 1;
}
if (defined($options{"t"})) {
    $webtask_id = $options{"t"};
125
    $webtask = WebTask->Lookup($webtask_id);
126
    if (!defined($webtask)) {
127
	fatal("Could not get webtask object");
128
129
    }
    $webtask->AutoStore(1);
130
131
132
133
}
if (@ARGV < 1) {
    usage();
}
134
my $action = shift(@ARGV);
135

136
137
138
139
if (getpwuid($UID) eq "nobody") {
    $this_user = User->ImpliedUser();
}
else  {
140
141
    $this_user = User->ThisUser();
}
142
143
144
145
146
# No guests allowed.
if (! defined($this_user)) {
    fatal("You ($UID) do not exist!");
}
$geniuser = GeniUser->CreateFromLocal($this_user);
147
148
149
150
151
152
153
154
155
156

if ($action eq "create") {
    exit(DoCreate());
}
elsif ($action eq "delete") {
    exit(DoDelete());
}
elsif ($action eq "refresh") {
    exit(DoRefresh());
}
157
158
159
160
161
162
elsif ($action eq "modify") {
    exit(DoModify());
}
elsif ($action eq "extend") {
    exit(DoExtend());
}
163
164
165
elsif ($action eq "snapshot") {
    exit(DoSnapshot());
}
166
167
168
elsif ($action eq "getcredential") {
    exit(DoGetCredential());
}
169
170
171
elsif ($action eq "approve") {
    exit(DoApprove());
}
172
173
174
175
176
177
178
179
180
181
182
183
else {
    usage();
}
exit(1);

#
# 
#
sub DoCreate()
{
    my $usage = sub {
	print STDERR "Usage: manage_dataset create ".
184
	    "[-t type] [-f fstype] [-e expiration] ".
185
	    "[-R global|project] [-W creator|project] ".
186
	    "-a am_urn -s size pid/name\n";
187
188
	exit(-1);
    };
189
    my $aggregate_urn;
190
191
192
    my $errmsg;
    my $pid;
    my $expires;
193
    my $size = 0;
194
195
    my $type = "stdataset";
    my $fstype;
196
197
    my $read_access;
    my $write_access;
198
    # imdataset snapshot info.
199
    my ($instance,$aggregate,$nodeid,$bsname);
200
    
201
    my $optlist = "ds:t:e:f:w:p:R:W:I:i:a:";
202
203
204
205
206
207
208
209
210
211
    my %options = ();
    if (! getopts($optlist, \%options)) {
	&$usage();
    }
    if (defined($options{"d"})) {
	$debug = 1;
    }
    if (defined($options{"t"})) {
	$type = $options{"t"};
	&$usage()
212
213
	    if (! ($type eq "stdataset" || $type eq "ltdataset" ||
		   $type eq "imdataset"));
214
    }
215
216
217
218
219
220
    if (defined($options{"a"})) {
	$aggregate_urn = $options{"a"};
    }
    elsif ($type ne "imdataset") {
	&$usage();
    }
221
    if ($type eq "imdataset") {
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
	if (!exists($options{"i"})) {
	    print STDERR "Must provide -i opton for imdatasets\n";
	    &$usage();
	}
	if (!exists($options{"I"})) {
	    print STDERR "Must provide -I opton for imdatasets\n";
	    &$usage();
	}
	$instance = APT_Instance->Lookup($options{"i"});
	if (!defined($instance)) {
	    fatal("Instance does not exist!");
	}
	($nodeid,$bsname) = split(",", $options{"I"});
	if (! (defined($nodeid) && defined($bsname))) {
	    print STDERR "Improper -I opton for imdatasets\n";
237
238
	    &$usage();
	}
239
240
241
242
	$aggregate = $instance->FindAggregateByNodeId($nodeid);
	if (!defined($aggregate)) {
	    fatal("Could not find aggregate for $nodeid");
	}
243
	$aggregate_urn = $aggregate->aggregate_urn();
244
    }
245
    else {
246
	if (!APT_Dataset::ValidBlockstoreBackend($aggregate_urn)) {	
247
248
249
	    fatal("Invalid cluster selection");
	}
    }
250
251
252
253
254
255
256
257
258
259
260
261
    if (defined($options{"f"})) {
	$fstype = $options{"f"};
	&$usage()
	    if ($fstype !~ /^(ext2|ext3|ext4|ufs|ufs2)$/);
    }
    if (defined($options{"R"})) {
	$read_access = $options{"R"};
	&$usage()
	    if ($read_access !~ /^(global|project)$/);
    }
    if (defined($options{"W"})) {
	$write_access = $options{"W"};
Leigh B Stoller's avatar
Leigh B Stoller committed
262
	&$usage()
263
	    if ($write_access !~ /^(creator|project)$/);
Leigh B Stoller's avatar
Leigh B Stoller committed
264
    }
265
266
267
268
269
    if (defined($options{"s"})) {
	if ($options{"s"} =~ /^(\d+)$/) {
	    $size = $1;
	}
	elsif ($options{"s"} =~ /^(\d+)(\w+)$/) {
Leigh B Stoller's avatar
Leigh B Stoller committed
270
271
	    # Converter wants upper case.
	    $size = Blockstore::ConvertToMebi(uc($options{"s"}));
272
273
274
	    if ($size < 0) {
		fatal("Could not parse size.");
	    }
275
276
277
	    if ($size <= 1) {
		fatal("Size too small, try a little bigger");
	    }
278
279
280
281
282
283
284
285
286
287
288
289
	}
	else {
	    &$usage();
	}
    }
    if (defined($options{"e"})) {
	$expires = str2time($options{"e"});
	if (!defined($expires)) {
	    fatal("Could not parse expiration date.");
	}
	$expires = $options{"e"};
    }
290
    
291
    &$usage()
292
	if (@ARGV != 1 || !defined($aggregate_urn) ||
293
	    ($type ne "imdataset" && !defined($size)) ||
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
	    ($type eq "stdataset" && !defined($expires)));
    my $name = shift(@ARGV);

    if ($name =~ /^([-\w]+)\/([-\w]+)$/) {
	$pid  = $1;
	$name = $2;
    }
    else {
	fatal("Dataset name $name not in the form <pid>/<name>.");
    }
    my $project = Project->Lookup($pid);
    if (!defined($project)) {
	fatal("No such project");
    }
    if (!$project->AccessCheck($this_user, TB_PROJECT_CREATELEASE())) {
	fatal("Not enough permission in project");
    }
    &$usage()
	if ($type eq "stdataset" && !defined($expires));

    if (APT_Dataset->Lookup("$pid/$name")) {
	fatal("Dataset already exists!");
    }

318
319
320
321
322
323
324
    # Check for expired certs and speaksfor.
    if (my $retval = APT_Geni::VerifyCredentials($geniuser, \$errmsg)) {
	if ($retval) {
	    ($retval < 0 ? fatal($errmsg) : uerror($errmsg));
	}
    }

325
326
327
328
    my $blob = {
	"dataset_id"     => $name,
	"pid"            => $project->pid(),
	"pid_idx"        => $project->pid_idx,
329
330
	"gid"            => $project->pid(),
	"gid_idx"        => $project->pid_idx,
331
332
333
334
335
336
337
338
	"creator_uid"    => $this_user->uid(),
	"creator_idx"    => $this_user->uid_idx(),
	"aggregate_urn"  => $aggregate_urn,
	"type"           => $type,
	"size"           => $size,
    };
    $blob->{"fstype"} = $fstype
	if (defined($fstype));
Leigh B Stoller's avatar
Leigh B Stoller committed
339
    $blob->{"expires"} = TBDateStringLocal($expires)
340
	if (defined($expires));
341
342
343
344
    $blob->{"read_access"} = $read_access
	if (defined($read_access));
    $blob->{"write_access"} = $write_access
	if (defined($write_access));
345

346
347
348
349
350
351
352
353
354
355
356
357
    #
    # Always create a webtask for tracking image or allocation status.
    # This is an internal webtask, not the one used on the command line.
    #
    my $dwebtask = WebTask->Create();
    if (!defined($dwebtask)) {
	$errmsg = "Could not create webtask object";
	goto failed;
    }
    $dwebtask->AutoStore(1);
    $blob->{"webtask_id"} = $dwebtask->task_id();

358
359
    my $dataset = APT_Dataset->Create($blob);
    if (!defined($dataset)) {
360
	$dwebtask->Delete();
361
	fatal("Error creating dataset object");
362
    }
363
    
364
365
366
367
368
369
370
371
    # new dataset is returned locked. If we have instance, try to lock
    # that now, else its a failure.
    if ($type eq "imdataset" && defined($instance)) {
	if ($instance->Lock()) {
	    $errmsg = "Instance is busy, cannot snapshot data";
	    goto failed;
	}
    }
372
373
374
375
376
377
    #
    # Ask the aggregate to create the dataset. 
    #
    my $response = $dataset->CreateDataset();
    if ($response->code() != GENIRESPONSE_SUCCESS) {
	$errmsg = "CreateDataset failed: ". $response->output() . "\n";
378
379
	$instance->Unlock()
	    if (defined($instance));
380
381
382
	goto failed;
    }
    $blob = $response->value();
383
384
    $dataset->Update({"remote_uuid" => $blob->{"uuid"},
		      "remote_urn"  => $blob->{"urn"}});
385
386
387
    if (exists($blob->{'url'}) && $blob->{'url'} ne "") {
	$dataset->Update({"remote_url" => $blob->{"url"}});
    }
388

389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
    if ($type ne "imdataset") {
	#
	# Okay, this is silly; there is no distinct state for resource
	# allocation. The other side now tells us expicitly that the
	# dataset (lease) was approved or not. If not approved there is
	# no reason to continue, we just want to tell the user in the
	# web UI an send email to local tbops.
	#
	# If approved, then it is probably busy and we need to wait for
	# it to finish.
	#
	if (! $blob->{'approved'}) {
	    $dataset->Update({"state" => "unapproved"});
	    if (defined($webtask)) {
		$webtask->needapproval(1);
		$webtask->unapproved_reason($blob->{'unapproved_reason'})
		    if (exists($blob->{'unapproved_reason'}));
	    }
	    $dataset->Unlock();
	    return 0;
	}
	if ($blob->{"busy"}) {
	    # Will poll for completion below.
	    $dataset->Update({"state" => "busy"});
	}
	else {
	    # This should no longer happen.
	    $dataset->Update({"state" => $blob->{"state"}});
	    $dataset->Unlock();
	    return 0;
	}
420
421
422
    }
    else {
	$dataset->Update({"state" => $blob->{"state"}});
423
424
	# Not doing a snapshot so just exit. Not sure this actually happens.
	if (!defined($instance)) {
425
426
427
	    $dataset->Unlock();
	    return 0;
	}
428
    }
429

430
431
432
433
    #
    # Handoff to snapshot if an imdataset.
    #
    if ($type eq "imdataset" &&
434
	DoSnapShotInternal($dataset, $aggregate, $bsname, $nodeid, \$errmsg)) {
435
436
437
438
439
440
441
442
	$response = $dataset->DeleteDataset();
	if ($response->code() == GENIRESPONSE_SUCCESS ||
	    $response->code() == GENIRESPONSE_SEARCHFAILED) {
	    $instance->Unlock();
	    goto failed;
	}
	# We want to keep the local dataset record around since we could
	# not delete it remotely.
443
	$instance->Unlock();
444
445
	# This will set the webtask, see below.
	fatal($errmsg);
446
    }
447
    if (PollDatasetStatus($dataset, $aggregate, \$errmsg)) {
448
449
	# Exit and let child poll
	exit(0);
450
451
    }
    $dataset->Unlock();
452
    $instance->Unlock() if (defined($instance));
453
454
455
456
457
458
459
460
461
462
463
464
465
466
    return 0;

  failed:
    $dataset->Delete()
	if (defined($dataset));
    # This will set the webtask, see below.
    fatal($errmsg);
}

#
# Delete
#
sub DoDelete()
{
467
    my $errmsg  = "Could not delete dataset";
468
469
470
471
472
473
474
475
476
477
    
    if (@ARGV != 1) {
	fatal("usage: $0 delete pid/name");
    }
    my $token   = shift(@ARGV);
    my $dataset = APT_Dataset->Lookup($token);
    if (!defined($dataset)) {
	fatal("No such dataset");
    }
    if ($dataset->Lock()) {
478
	uerror("dataset is busy, cannot lock it");
479
480
    }
    my $response = $dataset->DeleteDataset();
481
482
483
484
    if (!defined($response)) {
	$errmsg = "RPC Error calling DeleteDataset";
	goto failed;
    }
Leigh B Stoller's avatar
Leigh B Stoller committed
485
    if ($response->code() != GENIRESPONSE_SUCCESS &&
486
487
	$response->code() != GENIRESPONSE_SEARCHFAILED &&
	$response->code() != GENIRESPONSE_BUSY) {
488
489
490
	$errmsg = "DeleteDataset failed: ". $response->output() . "\n";
	goto failed;
    }
491
    if ($response->code() == GENIRESPONSE_BUSY) {
492
493
494
	$dataset->Unlock();
	uerror("dataset was busy at the remote cluster, try again later");
    }
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
    $dataset->Delete();
    return 0;

  failed:
    $dataset->Unlock();
    # This will set the webtask, see below.
    fatal($errmsg);
}

#
# Refresh
#
sub DoRefresh()
{
    my $errmsg;
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
    my $errcode;

    my $usage = sub {
	print STDERR "Usage: manage_dataset refresh [-p] uuid\n";
	exit(-1);
    };
    my $optlist = "p";
    my $poll    = 0;
    my %options = ();
    if (! getopts($optlist, \%options)) {
	&$usage();
    }
    if (defined($options{"p"})) {
	$poll = 1;
    }
525
    if (@ARGV != 1) {
526
	&$usage();
527
528
529
530
531
532
533
    }
    my $token   = shift(@ARGV);
    my $dataset = APT_Dataset->Lookup($token);
    if (!defined($dataset)) {
	fatal("No such dataset");
    }
    if ($dataset->Lock()) {
534
	uerror("dataset is busy, cannot lock it");
535
    }
536
537
538
539
540
541
542
543
544
545
546
    if ($poll) {
	if (PollDatasetStatus($dataset, $dataset->GetAggregate(), \$errmsg)) {
	    # Parent exits;
	    return 0;
	}
    }
    else {
	$errcode = DoRefreshInternal($dataset, \$errmsg);
	goto failed
	    if ($errcode);
    }
547
548
549
550
551
552
    $dataset->Unlock();
    return 0;

  failed:
    $dataset->Unlock();
    # This will set the webtask, see below.
553
    ($errcode < 0 ? fatal($errmsg) : uerror($errmsg, $errcode));
554
555
556
557
558
559
560
}

sub DoRefreshInternal($$)
{
    my ($dataset, $pmesg) = @_;
    
    my $response = $dataset->DescribeDataset();
561
562
563
564
    if (!defined($response)) {
	$$pmesg = "RPC Error calling DescribeDataset";
	return -1;
    }
565
    if ($response->code() != GENIRESPONSE_SUCCESS) {
566
	if ($response->code() == GENIRESPONSE_SEARCHFAILED) {
567
	    $$pmesg = "Dataset no longer exists at the remote cluster\n";
568
	    return GENIRESPONSE_SEARCHFAILED;
569
570
571
572
	}
	else {
	    $$pmesg = "DescribeDataset failed: ". $response->output() . "\n";
	}
573
574
575
	return -1;
    }
    my $blob = $response->value();
576
    print Dumper($blob);
577
    
578
579
    $dataset->Update({"last_used" => TBDateStringLocal($blob->{"lastused"}),
		      "expires"   => TBDateStringLocal($blob->{"expires"})});
580
581
    $dataset->Update({"updated"   => TBDateStringLocal($blob->{"updated"})})
	if ($blob->{"updated"});
582
583
584

    if ($blob->{"busy"}) {
	$dataset->Update({"state" => "busy"});
585
	if ($dataset->type() eq "imdataset") {
586
587
588
589
590
	    $dataset->webtask()->image_size($blob->{'image_size'}) 	
		if (exists($blob->{'image_size'}));
	    $dataset->webtask()->image_status($blob->{'image_status'})
		if (exists($blob->{'image_status'}));
	    $dataset->webtask()->Store();
591
	}
592
593
594
    }
    else {
	$dataset->Update({"state" => $blob->{"state"}});
595
596
	if ($dataset->type() eq "imdataset") {
	    $dataset->Update({"size" => $blob->{"size"}});
597
598
599
600
601
	    $dataset->webtask()->image_size($blob->{'image_size'}) 	
		if (exists($blob->{'image_size'}));
	    $dataset->webtask()->image_status($blob->{'image_status'})
		if (exists($blob->{'image_status'}));
	    $dataset->webtask()->Store();
602
	}
603
    }
604
605
606
    return 0;
}

607
608
609
610
611
612
613
614
615
#
# Modify
#
sub DoModify()
{
    my $errmsg;
    
    my $usage = sub {
	print STDERR "Usage: manage_dataset modify ".
616
	    "[-R global|project] [-W creator|project] pid/name\n";
617
618
	exit(-1);
    };
619
    my $optlist = "R:W:";
620
621
622
623
624
625
626
627
628
629
630
631
    my %options = ();
    if (! getopts($optlist, \%options)) {
	&$usage();
    }
    if (@ARGV != 1) {
	&$usage();
    }
    my $token   = shift(@ARGV);
    my $dataset = APT_Dataset->Lookup($token);
    if (!defined($dataset)) {
	fatal("No such dataset");
    }
632
633
634
    my $blob = {};
    if (defined($options{"R"})) {
	my $read_access = $options{"R"};
635
	&$usage()
636
637
	    if ($read_access !~ /^(global|project)$/);
	$blob->{'read_access'} = $read_access;
638
    }
639
640
641
642
643
    if (defined($options{"W"})) {
	my $write_access = $options{"W"};
	&$usage()
	    if ($write_access !~ /^(creator|project)$/);
	$blob->{'write_access'} = $write_access;
644
645
    }
    if ($dataset->Lock()) {
646
	uerror("dataset is busy, cannot lock it");
647
    }
648
649
    if (keys(%$blob)) {
	if ($dataset->Update($blob)) {
650
651
652
653
654
	    $errmsg = "Could not update privacy settings!";
	    goto failed;
	}
    }
    my $response = $dataset->ModifyDataset();
655
656
657
658
    if (!defined($response)) {
	$errmsg = "RPC Error calling ModifyDataset";
	goto failed;
    }
659
660
661
662
663
664
665
666
667
    if ($response->code() != GENIRESPONSE_SUCCESS) {
	if ($response->code() == GENIRESPONSE_SEARCHFAILED) {
	    $errmsg = "Dataset no longer exists at the target\n";
	}
	else {
	    $errmsg = "ModifyDataset failed: ". $response->output() . "\n";
	}
	goto failed;
    }
668
    $blob = $response->value();
669
670
671
    if ($dataset->type() ne "imdataset") {
	$dataset->Update({"expires" => TBDateStringLocal($blob->{"expires"})});
    }
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
    $dataset->Unlock();
    return 0;

  failed:
    $dataset->Unlock();
    # This will set the webtask, see below.
    fatal($errmsg);
}

#
# Extend
#
sub DoExtend()
{
    my $errmsg;
    
    my $usage = sub {
	print STDERR "Usage: manage_dataset extend pid/name\n";
	exit(-1);
    };
    if (@ARGV != 1) {
	&$usage();
    }
    my $token   = shift(@ARGV);
    my $dataset = APT_Dataset->Lookup($token);
    if (!defined($dataset)) {
	fatal("No such dataset");
    }
    if ($dataset->Lock()) {
701
	uerror("dataset is busy, cannot lock it");
702
703
    }
    my $response = $dataset->ExtendDataset();
704
705
706
707
    if (!defined($response)) {
	$errmsg = "RPC Error calling ExtendDataset";
	goto failed;
    }
708
709
710
711
712
713
714
715
716
717
718
    if ($response->code() != GENIRESPONSE_SUCCESS) {
	if ($response->code() == GENIRESPONSE_SEARCHFAILED) {
	    $errmsg = "Dataset no longer exists at the target\n";
	}
	else {
	    $errmsg = "ExtendDataset failed: ". $response->output() . "\n";
	}
	goto failed;
    }
    my $blob = $response->value();
    $dataset->Update({"expires" => TBDateStringLocal($blob->{"expires"})});
719
720
721
    if (exists($blob->{'state'})) {
	$dataset->Update({"state" => $blob->{'state'}});
    }
722
723
724
725
726
727
728
729
730
    $dataset->Unlock();
    return 0;

  failed:
    $dataset->Unlock();
    # This will set the webtask, see below.
    fatal($errmsg);
}

731
732
733
734
735
736
#
# Snapshot an image backed dataset
#
sub DoSnapshot()
{
    my $errmsg;
737
    my ($copyback_uuid, $sha1hash);
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
    
    my $usage = sub {
	print STDERR "Usage: manage_dataset snapshot ".
	    "-i instance -b bsname pid/name nodeid\n";
	exit(-1);
    };
    my $optlist = "b:i:";
    my %options = ();
    if (! getopts($optlist, \%options)) {
	&$usage();
    }
    &$usage()
	if (! (@ARGV == 2 && exists($options{"b"}) && exists($options{"i"})));
    
    my $bsname  = $options{"b"};
    my $token   = shift(@ARGV);
    my $nodeid  = shift(@ARGV);
    my $dataset = APT_Dataset->Lookup($token);
    if (!defined($dataset)) {
	fatal("No such dataset");
    }
    if ($dataset->type() ne "imdataset") {
760
	fatal("Only image backed datasets supported");
761
762
763
    }
    my $instance = APT_Instance->Lookup($options{"i"});
    if (!defined($instance)) {
764
765
	fatal("No such instance");
    }
766
767
768
769
    my $aggregate = $instance->FindAggregateByNodeId($nodeid);
    if (!defined($aggregate)) {
	fatal("Could not find aggregate for $nodeid");
    }
770
    if (GetSiteVar("protogeni/use_imagetracker")) {
771
772
	if (DoImageTrackerStuff($dataset, $aggregate,
				\$copyback_uuid,\$sha1hash,\$errmsg)) {
773
774
775
	    fatal("Could not get info from image tracker");
	}
    }
776
    if ($dataset->Lock()) {
777
	uerror("dataset is busy, cannot lock it");
778
779
    }
    if ($instance->Lock()) {
780
781
	$dataset->Unlock();
	uerror("instance is busy, cannot lock it");
782
    }
783
784
785
786
787
788
789
    # Clear the webtask, starting a new snapshot.
    $dataset->webtask()->Reset();
    # These three are convenience for the web server to give feedback.
    $dataset->webtask()->aggregate_urn($aggregate->aggregate_urn());
    $dataset->webtask()->client_id($nodeid);
    $dataset->webtask()->instance($instance->uuid());
    
790
791
    if (defined($copyback_uuid)) {
	# Tell the imaging modal.
792
	$dataset->webtask()->copyback_uuid($copyback_uuid);
793
794
795
796
797
	# For polling below.
	$dataset->_copyback_uuid($copyback_uuid);
	$dataset->_sha1hash("$sha1hash");
	$dataset->_copying(0);
    }
798
799
800
    $dataset->webtask()->Store();
    $dataset->webtask()->AutoStore(1);
    
801
    if (DoSnapShotInternal($dataset, $aggregate, $bsname, $nodeid, \$errmsg)) {
802
803
	goto failed;
    }
804
    if (PollDatasetStatus($dataset, $aggregate, \$errmsg)) {
805
806
807
	# Exit and let child poll
	exit(0);
    }
808
809
810
811
812
    $instance->Unlock();
    $dataset->Unlock();
    return 0;
    
  failed:
813
    $instance->Unlock();
814
815
816
817
818
819
820
    $dataset->Unlock();
    # This will set the webtask, see below.
    fatal($errmsg);
}

sub DoSnapShotInternal($$$$$)
{
821
    my ($dataset, $aggregate, $bsname, $nodeid, $perrmsg) = @_;
822
823
    my $errmsg;
    
824
    my $manifest = GeniXML::Parse($aggregate->manifest());
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
    if (! defined($manifest)) {
	$errmsg = "Could not parse manifest";
	goto failed;
    }
    my $sliver_urn;
    my @nodes = GeniXML::FindNodes("n:node", $manifest)->get_nodelist();
    foreach my $node (@nodes) {
	my $client_id = GeniXML::GetVirtualId($node);
	if ($nodeid eq $client_id) {
	    $sliver_urn = GeniXML::GetSliverId($node);
	    #
	    # But check that the bsname is on this node.
	    #
	    my $found = 0;
	    foreach my $blockref
		(GeniXML::FindNodesNS("n:blockstore", $node,
				      $GeniXML::EMULAB_NS)->get_nodelist()) {
		    my $name = GeniXML::GetText("name", $blockref);
		    if ($name eq $bsname) {
			$found = 1;
			last;
		    }
	    }
	    if (!$found) {
		$errmsg = "No such blockstore $bsname on node $nodeid";
		goto failed;
	    }
	    last;
	}
    }
    if (!defined($sliver_urn)) {
	$errmsg = "Could not find node '$nodeid' in manifest";
	goto failed;
    }
859
    my $response = $aggregate->CreateImage($sliver_urn,
860
					   $dataset->dataset_id(), 0,
861
					   $dataset->_copyback_uuid(),
862
					   $bsname, 0, 0, 0);
863
864
865
866
867
    if ($response->code() != GENIRESPONSE_SUCCESS) {
	$errmsg = "SnapshotDataset failed: ". $response->output() . "\n";
	goto failed;
    }
    $dataset->Update({"state" => "busy"});
868
869
870
871
872
873
874
875
876
    #
    # If we are taking the snapshot at a different cluster, we have
    # to poll that cluster via ImageInfo() instead of DescribeDataset().
    # We need the image urn to do that in the polling loop.
    #
    if ($aggregate->aggregate_urn() ne $dataset->aggregate_urn()) {
	my ($image_urn) = @{ $response->value() };	
	$dataset->_image_urn($image_urn);
    }
877
    return 0;
878

879
  failed:
880
881
    $$perrmsg = $errmsg;
    return -1;
882
}
883
884
885
886

#
# Poll for snapshot status.
#
887
sub PollDatasetStatus($$$)
888
{
889
    my ($dataset, $aggregate, $perrmsg) = @_;
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
    my $project = $dataset->GetProject();
    my $dname   = $dataset->dataset_id();
    my $logfile;

    #
    # If busy, then allocation is in progress. We leave it locked and
    # poll in the background for a while, hoping for it to eventually
    # stop being busy. Eventually might have to replace this, since
    # polling got any non-small length of time will lead to trouble.
    #
    if (! $debug) {
        $logfile = TBMakeLogname("createdataset");

	if (my $childpid = TBBackGround($logfile)) {
	    return $childpid;
	}
	# Let parent exit;
	sleep(2);
    }
909
    $dataset->webtask()->SetProcessID($PID);
910

911
912
913
914
915
916
    print "State: " . $dataset->state() . "\n";
    if (defined($dataset->_copyback_uuid())) {
	my $copyback_uuid = $dataset->_copyback_uuid();
	my $sha1hash = $dataset->_sha1hash();
	print "hash: $sha1hash, copyback_uuid: $copyback_uuid\n";
    }
917
    my $seconds  = 1200;
918
    my $interval = 10;
919
920
921
    
    while ($seconds > 0) {
	$seconds -= $interval;
922
923
924
925
926
927
928
929
930
931
	#
	# The second part of the test is to distingush between an
	# imdataset snapshot at its home aggregate, and an imdataset
	# taking place at another cluster and thus needing a copy
	# back. We use a different polling function for the later,
	# since there is no dataset to ask about, just an image that
	# is doing a snapshot. This needs more thought, its not a
	# great way to do this.
	#
	if ($dataset->type() =~ /^(lt|st)dataset$/ ||
932
	    $aggregate->aggregate_urn() eq $dataset->aggregate_urn()) {
933
934
	    my $errcode = DoRefreshInternal($dataset, $perrmsg);
	    if ($errcode) {
935
		print STDERR $$perrmsg;
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
		if ($errcode == GENIRESPONSE_SEARCHFAILED) {
		    #
		    # The dataset is gone, so it failed allocation.
		    # This should not happen for an imdataset of course.
		    # Mark the dataset as failed, we do not know why
		    # though, the allocation is asynchronous, and the error
		    # went out in email. But we can tell the user in the
		    # web UI.
		    #
		    $dataset->Update({"state" => "failed"});
		    $dataset->webtask()->output("allocation failure");
		    $dataset->webtask()->Exited(GENIRESPONSE_SEARCHFAILED);
		    last;
		}
		# Otherwise we keep trying. 
951
952
953
954
955
956
957
958
959
960
		sleep($interval);
		next;
	    }
	}
	else {
	    if (PollImageStatus($dataset, $aggregate, $perrmsg)) {
		print STDERR $$perrmsg;
		sleep($interval);
		next;
	    }
961
962
	}
	if ($dataset->state() eq "valid") {
963
	    print "Dataset is now valid\n";
964
965
	    $project->SendEmail($this_user->email(),
			"Your dataset is now ready to use",
966
			"Dataset '$dname' is now ready to use.\n",
967
				$project->LogsEmailAddress(), undef, $logfile);
968
	    $dataset->webtask()->Exited(0);
969
970
971
972
	    last;
	}
	sleep($interval);
    }
973
    unlink($logfile) if (defined($logfile));
974
975
    $dataset->webtask()->Exited(-1)
	if ($seconds <= 0);
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
    return 0;
}

#
# GetCredential
#
sub DoGetCredential()
{
    my $errmsg  = "Could not get credential for dataset";
    my ($authority, $certificate, $credential);
    
    my $optlist = "a:f";
    my %options = ();
    if (! getopts($optlist, \%options)) {
	usage();
    }
    if (defined($options{"a"})) {
	my $urn = GeniHRN->new($options{"a"});
	fatal("Not a valid authority URN")
	    if (!defined($urn));

	#
	# Load the cert to act as caller context in case we need to go
	# to the clearinghouse.
	#
	$certificate = GeniCertificate->LoadFromFile($SACERT);
	if (!defined($certificate)) {
	    fatal("Could not load certificate from $SACERT\n");
	}
	Genixmlrpc->SetContext(Genixmlrpc->Context($certificate));
	
	$authority = GeniAuthority->CreateFromRegistry($urn->id(), $urn);
	fatal("No such authority")
	    if (!defined($authority));
    }
    if (@ARGV != 1) {
	fatal("usage: $0 getcredential [-a authority] pid/name");
    }
    my $token   = shift(@ARGV);
    my $dataset = APT_Dataset->Lookup($token);
    if (!defined($dataset)) {
	fatal("No such dataset");
    }
    if ($dataset->Lock()) {
	uerror("dataset is busy, cannot lock it");
    }
    #
    # If we have a stored unexpired credential, we can just use that.
    #
    if (!defined($options{"f"}) &&
	$dataset->credential_string() && $dataset->credential_string() ne "") {
	$credential =
	    GeniCredential->CreateFromSigned($dataset->credential_string());
	goto haveit
	    if (defined($credential) && !$credential->IsExpired());
    }
    my $response = $dataset->GetCredential();
    if (!defined($response)) {
	$errmsg = "RPC Error calling GetCredential";
	goto failed;
    }
    if ($response->code() != GENIRESPONSE_SUCCESS &&
	$response->code() != GENIRESPONSE_SEARCHFAILED &&
	$response->code() != GENIRESPONSE_BUSY) {
	$errmsg = "GetCredential failed: ". $response->output() . "\n";
	goto failed;
    }
    if ($response->code() == GENIRESPONSE_BUSY) {
	$dataset->Unlock();
	uerror("dataset was busy at the remote cluster, try again later");
    }
1047
1048
1049
1050
    if ($response->code() == GENIRESPONSE_SEARCHFAILED) {
	$dataset->Unlock();
	uerror("dataset could not be found at the remote cluster");
    }
1051
    $credential = GeniCredential->CreateFromSigned($response->value());
1052
1053
1054
1055
    if (!defined($credential)) {
	$dataset->Unlock();
	fatal("Could not parse new credential")
    }
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
    $dataset->Update({"credential_string" => $response->value()});
  haveit:
    if (defined($authority)) {
	my $delegated = $credential->Delegate($authority);
	$delegated->Sign($certificate);
	$credential = $delegated;
    }
    print $credential->asString();
    $dataset->Unlock();
    return 0;

  failed:
    $dataset->Unlock();
    # This will set the webtask, see below.
    fatal($errmsg);
}

1073
sub DoImageTrackerStuff($$$$$)
1074
{
1075
1076
1077
    my ($dataset, $aggregate, $puuid, $phash, $perrmsg) = @_;
    my $remote_urn = GeniHRN->new($dataset->remote_urn());
    my $aggregate_urn = GeniHRN->new($aggregate->aggregate_urn());
1078
1079
    my $errmsg;

1080
1081
1082
1083
1084
1085
1086
    #
    # If the dataset is being used on the cluster where it lives, then
    # there is no need for any of this.
    #
    return 0
	if (lc($remote_urn->domain()) eq lc($aggregate_urn->domain()));
    
1087
1088
1089
1090
1091
1092
1093
1094
1095
    Genixmlrpc->SetContext(APT_Geni::GeniContext());
    my $blob = GeniImage::GetImageData($remote_urn, \$errmsg);
    Genixmlrpc->SetContext(undef);
    
    if (!defined($blob)) {
	$$perrmsg = "Could not get info from the image server for ".
	    "$remote_urn:\n" . $errmsg;
	    return 1;
    }
1096

1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
    $$puuid = $blob->{'version_uuid'} if (defined($puuid));
    $$phash = $blob->{'sha1hash'} if (defined($phash));
    return 0;
}

sub PollImageStatus($$$)
{
    my ($dataset, $aggregate, $perrmsg) = @_;
    my $image_urn = $dataset->_image_urn();
    my $copyback_uuid = $dataset->_copyback_uuid();

    #
    # Once we hit the copyback phase, we have to ask the image tracker
    # for info to figure out when the copyback is done.
    #
    if ($dataset->_copying()) {
	my $sha1hash;
	
1115
1116
	if (DoImageTrackerStuff($dataset, $aggregate,
				undef, \$sha1hash, $perrmsg)) {
1117
1118
1119
	    print STDERR $perrmsg . "\n";
	    # Give up.
	    $dataset->Update({"state" => "valid"});
1120
	    $dataset->webtask()->image_status("ready");
1121
1122
1123
1124
	}
	if ("$sha1hash" eq $dataset->_sha1hash()) {
	    # Done!
	    $dataset->Update({"state" => "valid"});
1125
	    $dataset->webtask()->image_status("ready");
1126
1127
1128
1129
	}
	return 0;
    }
    else {
1130
1131
	print "Getting Image Info\n";
	
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
	my $response = $aggregate->ImageInfo($image_urn);
	if ($response->code() != GENIRESPONSE_SUCCESS &&
	    $response->code() != GENIRESPONSE_RPCERROR &&
	    $response->code() != GENIRESPONSE_SERVER_UNAVAILABLE &&
	    $response->code() != GENIRESPONSE_BUSY) {
	    $$perrmsg = "Imageinfo failed: ". $response->output() . "\n";
	    return -1;
	}
	return 0
	    if ($response->code() == GENIRESPONSE_BUSY ||
		$response->code() == GENIRESPONSE_SERVER_UNAVAILABLE ||
		$response->code() == GENIRESPONSE_RPCERROR);

	my $blob = $response->value();
1146
	print Dumper($response->value());
1147
    
1148
	$dataset->webtask()->image_size($blob->{'size'})
1149
	    if (exists($blob->{'size'}));
1150
	$dataset->webtask()->image_status($blob->{'status'})
1151
1152
1153
	    if (exists($blob->{'status'}));
	if ($blob->{'status'} eq "ready") {
	    if ($copyback_uuid) {
1154
		$dataset->webtask()->image_status("copying");
1155
1156
1157
1158
1159
1160
1161
		$dataset->_copying(1);
	    }
	    else {
		$dataset->Update({"state" => "valid"});
	    }
	}
    }
1162
1163
1164
    return 0;
}

1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
#
# Approve
#
sub DoApprove()
{
    my $errmsg;
    my $logname;
    
    my $usage = sub {
	print STDERR "Usage: manage_dataset approve pid/name\n";
	exit(-1);
    };
    if (@ARGV != 1) {
	&$usage();
    }
    my $token   = shift(@ARGV);
    my $dataset = APT_Dataset->Lookup($token);
    if (!defined($dataset)) {
	fatal("No such dataset");
    }
    my $dname   = $dataset->dataset_id();

    if (!$this_user->IsAdmin()) {
	fatal("No permission to schedule reservation cancellation")
    }    
    if ($dataset->Lock()) {
	uerror("dataset is busy, cannot lock it");
    }
    my $project = $dataset->GetProject();
    my $creator = $dataset->GetCreator();
    
    my $response = $dataset->ApproveDataset();
    if (!defined($response)) {
	$errmsg = "RPC Error calling ExtendDataset";
	goto failed;
    }
    if ($response->code() != GENIRESPONSE_SUCCESS) {
	if ($response->code() == GENIRESPONSE_SEARCHFAILED) {
	    $errmsg = "Dataset no longer exists at the target\n";
	}
	else {
	    $errmsg = "ApproveDataset failed: ". $response->output() . "\n";
	}
	goto failed;
    }
    # No failure, change the state now so the web interface sees a change.
    $dataset->Update({"state" => "busy"});
    
    #
    # Now we want to poll for allocation completion so we can tell the
    # web interface when it is done (or failed). We know this when the
    # state changes to valid or failed.
    #
    if (! $debug) {
        $logname = TBMakeLogname("approvedataset");

	if (my $childpid = TBBackGround($logname)) {
	    exit(0);
	}
	# Let parent exit;
	sleep(2);
    }
    $dataset->webtask()->SetProcessID($PID);

    # Arbitrary max wait.
    my $seconds  = 1200;
    my $interval = 15;
    
    while ($seconds > 0) {
	my $errcode = DoRefreshInternal($dataset, \$errmsg);
	if ($errcode) {
	    print STDERR $errmsg;
	    if ($errcode == GENIRESPONSE_SEARCHFAILED) {
		#
		# The dataset is gone, so it failed allocation.
		# This should not happen for an imdataset of course.
		# Mark the dataset as failed, we do not know why
		# though, the allocation is asynchronous, and the error
		# went out in email. But we can tell the user in the
		# web UI.
		#
		$dataset->Update({"state" => "failed"});
		$dataset->webtask()->output("allocation failure");
		$dataset->webtask()->Exited(GENIRESPONSE_SEARCHFAILED);
		last;
	    }
	    # Otherwise we keep trying. 
	    goto again;
	}
	if ($dataset->state() eq "valid") {
	    $creator->SendEmail("Your dataset is now ready to use",
				"Dataset '$dname' is now ready to use.\n",
				$project->LogsEmailAddress(), $TBOPS);
	    $dataset->webtask()->Exited(0);
	    last;
	}
	if ($dataset->state() eq "failed") {
	    $creator->SendEmail("Your dataset failed to allocate!",
				"Dataset '$dname' could not be allocated!\n",
				$project->LogsEmailAddress(), $TBOPS);
	    $dataset->webtask()->Exited(0);
	}
      again:
	$seconds -= $interval;
	sleep($interval);
    }
    if ($seconds <= 0) {
	$creator->SendEmail("Your dataset timed out while allocating!",
			    "Dataset '$dname' timed out while allocating!\n",
			    $project->LogsEmailAddress(), $TBOPS);
	$dataset->Update({"state" => "failed"});
	$dataset->webtask()->Exited(-1);
    }
    unlink($logname) if (defined($logname));
    $dataset->Unlock();
    return 0;

  failed:
    unlink($logname) if (defined($logname));
    $dataset->Unlock();
    # This will set the webtask, see below.
    fatal($errmsg);
}

1289
1290
1291
1292
1293
1294
1295
1296
sub fatal($)
{
    my ($mesg) = @_;

    if (defined($webtask)) {
	$webtask->output($mesg);
	$webtask->Exited(-1);
    }
1297
    print STDERR "$mesg\n";
1298
1299
1300
1301
    # Exit with negative status so web interface treats it as system error.
    exit(-1);
}

1302
sub uerror($;$)
1303
{
1304
1305
    my ($mesg, $code) = @_;
    $code = 1 if (!defined($code));
1306
1307
1308

    if (defined($webtask)) {
	$webtask->output($mesg);
1309
	$webtask->Exited($code);
1310
1311
    }
    print STDERR "$mesg\n";
1312
    exit($code);
1313
1314
}

1315
1316
1317
1318
1319
1320
1321
1322
sub escapeshellarg($)
{
    my ($str) = @_;

    $str =~ s/[^[:alnum:]]/\\$&/g;
    return $str;
}