APT_Profile.pm.in 29.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
#!/usr/bin/perl -wT
#
# Copyright (c) 2007-2014 University of Utah and the Flux Group.
# 
# {{{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/>.
# 
# }}}
#
package APT_Profile;

26 27 28 29 30 31 32 33 34 35 36
#
# Note about permissions bits.
#
# listed - The profile will be listed on the home page for anyone to see/use.
# public - Anyone can instantiate the profile, regardless of its listed bit
#          Say, if you send a URL to someone. 
# shared - Shared with logged in users. If not listed, then the default is
#          that only project members can see/use the profile, unless the public
#          is set (but they need a url). Shared says any logged in user can
#          see and use the profile.  

37 38 39 40 41 42 43 44 45 46
use strict;
use Carp;
use Exporter;
use vars qw(@ISA @EXPORT $AUTOLOAD);

@ISA    = "Exporter";
@EXPORT = qw ( );

# Must come after package declaration!
use EmulabConstants;
47
use emutil;
48
use emdb;
49
use APT_Dataset;
50
use GeniXML;
51
use GeniHRN;
52 53 54 55 56 57 58 59 60
use libtestbed;
use English;
use Data::Dumper;
use overload ('""' => 'Stringify');

# Configure variables
my $TB		  = "@prefix@";
my $TBOPS         = "@TBOPSEMAIL@";

61 62
my $debug = 0;

63 64 65 66 67 68 69 70
# Concat id/vers.
sub versid($)
{
    my ($self) = @_;

    return $self->profileid() . ":" . $self->version();
}

71 72 73 74 75 76 77 78 79 80
sub BlessRow($$)
{
    my ($class, $row) = @_;
    
    my $self           = {};
    $self->{'DBROW'}   = $row;

    bless($self, $class);
    return $self;
}
81 82

#
83
# Lookup. 
84
#
85
sub Lookup($$;$$)
86
{
87
    my ($class, $arg1, $arg2, $arg3) = @_;
88 89

    #
90 91
    # A single arg is either an index or "pid,profile[:version]" or
    # "pid/profile[:version]" string.
92 93 94
    #
    if (!defined($arg2)) {
	if ($arg1 =~ /^(\d*)$/) {
95
	    my $result =
96 97
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    " from apt_profiles as i ".
98 99 100 101 102 103 104 105
			    "left join apt_profile_versions as v on ".
			    "     v.profileid=i.profileid and ".
			    "     v.version=i.version ".
			    "where i.profileid='$arg1'");
	    return undef
		if (! $result || !$result->numrows);

	    return BlessRow($class, $result->fetchrow_hashref());
106 107
	}
	elsif ($arg1 =~ /^([-\w]*),([-\w\.\+]*)$/ ||
108 109
		$arg1 =~ /^([-\w]*)\/([-\w\.\+]*)$/) {
	    my $result =
110 111
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    " from apt_profiles as i ".
112 113 114 115 116 117 118 119 120 121 122 123
			    "left join apt_profile_versions as v on ".
			    "     v.profileid=i.profileid and ".
			    "     v.version=i.version ".
			    "where i.pid='$1' and i.name='$2'");
	    return undef
		if (! $result || !$result->numrows);

	    return BlessRow($class, $result->fetchrow_hashref());
	}
	elsif ($arg1 =~ /^([-\w]*),([-\w\.\+]*):(\d*)$/ ||
		$arg1 =~ /^([-\w]*)\/([-\w\.\+]*):(\d*)$/) {
	    my $result =
124 125
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    " from apt_profiles as i ".
126 127 128 129 130 131 132 133
			    "left join apt_profile_versions as v on ".
			    "     v.profileid=i.profileid ".
			    "where i.pid='$1' and i.name='$2' and ".
			    "      v.version='$3' and v.deleted is null");
	    return undef
		if (!$result || !$result->numrows);

	    return BlessRow($class, $result->fetchrow_hashref())
134 135
	}
	elsif ($arg1 =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/) {
136 137 138 139 140
	    #
	    # First look to see if the uuid is for the profile itself,
	    # which means current version. Otherwise look for a
	    # version with the uuid.
	    #
141
	    my $result =
142 143 144 145 146 147 148 149 150 151
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    "  from apt_profiles as i ".
			    "left join apt_profile_versions as v on ".
			    "     v.profileid=i.profileid and ".
			    "     v.version=i.version ".
			    "where i.uuid='$arg1'");
	    return undef
		if (! $result);
	    return BlessRow($class, $result->fetchrow_hashref())
		if ($result->numrows);
152

153 154 155 156 157 158 159
	    $result =
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    "  from apt_profile_versions as v ".
			    "left join apt_profiles as i on ".
			    "     v.profileid=i.profileid ".
			    "where v.uuid='$arg1' and ".
			    "      v.deleted is null");
160 161
	    return undef
		if (! $result || !$result->numrows);
162
	    return BlessRow($class, $result->fetchrow_hashref());
163 164 165
	}
	return undef;
    }
166 167 168
    elsif (!defined($arg3)) {
	if ($arg1 =~ /^\d+$/ && $arg2 =~ /^\d+$/) {
	    my $result =
169 170
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    "  from apt_profiles as i ".
171 172 173 174 175 176
			    "left join apt_profile_versions as v on ".
			    "     v.profileid=i.profileid ".
			    "where i.profileid='$arg1' and v.version='$arg2' ".
			    " and  v.deleted is null");
	    return undef
		if (! $result || !$result->numrows);
177

178 179 180 181
	    return BlessRow($class, $result->fetchrow_hashref());
	}
	elsif ($arg1 =~ /^[-\w]*$/ && $arg2 =~ /^([-\w\.\+]*):(\d+)$/) {
	    my $result =
182 183
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    " from apt_profiles as i ".
184 185 186 187 188 189
			    "left join apt_profile_versions as v on ".
			    "     v.profileid=i.profileid ".
			    "where i.pid='$arg1' and i.name='$1' and ".
			    "      v.version='$2'");
	    return undef
		if (! $result || !$result->numrows);
190

191 192 193 194
	    return BlessRow($class, $result->fetchrow_hashref());
	}
	elsif ($arg1 =~ /^[-\w]*$/ && $arg2 =~ /^[-\w\.\+]*$/) {
	    my $result =
195 196
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    "  from apt_profiles as i ".
197 198 199 200 201 202
			    "left join apt_profile_versions as v on ".
			    "     v.profileid=i.profileid and ".
			    "     v.version=i.version ".
			    "where i.pid='$arg1' and i.name='$arg2'");
	    return undef
		if (! $result || !$result->numrows);
203

204 205 206
	    return BlessRow($class, $result->fetchrow_hashref());
	}
	return undef;
207
    }
208 209 210 211
    else {
	if ($arg1 =~ /^[-\w]*$/ &&
	    $arg2 =~ /^[-\w\.\+]*$/ && $arg3 =~ /^\d+$/) {
	    my $result =
212 213
		DBQueryWarn("select i.*,v.*,i.uuid as profile_uuid ".
			    "  from apt_profiles as i ".
214 215 216 217 218 219
			    "left join apt_profile_versions as v on ".
			    "     v.profileid=i.profileid ".
			    "where i.pid='$arg1' and i.name='$arg2' and ".
			    "      v.version='$arg3' and v.deleted is null");
	    return undef
		if (!$result || !$result->numrows);
220

221 222 223 224
	    return BlessRow($class, $result->fetchrow_hashref());
	}
    }
    return undef;
225 226 227 228 229 230 231 232 233
}

AUTOLOAD {
    my $self  = $_[0];
    my $type  = ref($self) or croak "$self is not an object";
    my $name  = $AUTOLOAD;
    $name =~ s/.*://;   # strip fully-qualified portion

    # A DB row proxy method call.
234 235
    if (exists($self->{'DBROW'}->{$name})) {
	return $self->{'DBROW'}->{$name};
236 237 238 239 240 241 242 243 244
    }
    carp("No such slot '$name' field in class $type");
    return undef;
}

# Break circular reference someplace to avoid exit errors.
sub DESTROY {
    my $self = shift;

245
    $self->{'DBROW'} = undef;
246 247 248 249 250 251 252 253 254 255 256 257
}

#
# Refresh a class instance by reloading from the DB.
#
sub Refresh($)
{
    my ($self) = @_;

    return -1
	if (! ref($self));

258 259
    my $profileid = $self->profileid();
    my $version   = $self->version();
260 261
    
    my $query_result =
262 263
	DBQueryWarn("select * from apt_profile_versions ".
		    "where profileid='$profileid' and version='$version'");
264 265 266 267

    return -1
	if (!$query_result || !$query_result->numrows);

268
    $self->{'DBROW'} = $query_result->fetchrow_hashref();
269 270 271 272 273 274 275

    return 0;
}

#
# Create a profile
#
276
sub Create($$$$$$)
277
{
278
    my ($class, $parent, $project, $creator, $argref, $usrerr_ref) = @_;
279 280 281 282 283 284 285 286 287 288

    my $name    = DBQuoteSpecial($argref->{'name'});
    my $pid     = $project->pid();
    my $pid_idx = $project->pid_idx();
    my $uid     = $creator->uid();
    my $uid_idx = $creator->uid_idx();

    #
    # The pid/imageid has to be unique, so lock the table for the check/insert.
    #
289 290
    DBQueryWarn("lock tables apt_profiles write, apt_profile_versions write, ".
		"            emulab_indicies write")
291 292 293 294 295 296 297 298 299 300 301 302
	or return undef;

    my $query_result =
	DBQueryWarn("select name from apt_profiles ".
		    "where pid_idx='$pid_idx' and name=$name");

    if ($query_result->numrows) {
	DBQueryWarn("unlock tables");
	$$usrerr_ref = "Profile already exists in project!";
	return undef;
    }
    
303
    my $profileid = TBGetUniqueIndex("next_profile", undef, 1);
304 305
    my $puuid     = NewUUID();
    my $vuuid     = NewUUID();
306 307 308
    my $rspec     = DBQuoteSpecial($argref->{'rspec'});
    my $cquery    = "";
    my $vquery    = "";
309

310 311 312 313 314 315 316 317
    #
    # This part is common between the two tables.
    #
    $cquery .= "name=$name,profileid='$profileid'";
    $cquery .= ",pid='$pid',pid_idx='$pid_idx'";

    # And the versions table.
    $vquery  = $cquery;
318
    $vquery .= ",uuid='$vuuid',created=now()";
319 320 321 322 323 324 325 326
    $vquery .= ",creator='$uid',creator_idx='$uid_idx'";
    $vquery .= ",rspec=$rspec";

    # Set derived from pointer.
    if (defined($parent)) {
	$vquery .= ",parent_profileid=" . $parent->profileid();
	$vquery .= ",parent_version=" . $parent->version();
    }
327 328
    if (exists($argref->{'script'}) && $argref->{'script'} ne "") {
	$vquery .= ",script=" . DBQuoteSpecial($argref->{'script'});
329 330 331
	if (exists($argref->{'paramdefs'}) && $argref->{'paramdefs'} ne "") {
	    $vquery .= ",paramdefs=" . DBQuoteSpecial($argref->{'paramdefs'});
	}
332
    }
333

334
    # Back to the main table.
335
    $cquery .= ",uuid='$puuid'";
336
    $cquery .= ",public=1"
337
	if (exists($argref->{'public'}) && $argref->{'public'});
338
    $cquery .= ",listed=1"
339
	if (exists($argref->{'listed'}) && $argref->{'listed'});
340
    $cquery .= ",shared=1"
341
	if (exists($argref->{'shared'}) && $argref->{'shared'});
342 343
    $cquery .= ",topdog=1"
	if (exists($argref->{'topdog'}) && $argref->{'topdog'});
344

345 346 347 348 349 350 351 352 353
    # Create the main entry:
    if (! DBQueryWarn("insert into apt_profiles set $cquery")) {
	DBQueryWarn("unlock tables");
	tberror("Error inserting new apt_profiles record!");
	return undef;
    }
    # And the versions entry.
    if (! DBQueryWarn("insert into apt_profile_versions set $vquery")) {
	DBQueryWarn("delete from apt_profiles where profileid='$profileid'");
354
	DBQueryWarn("unlock tables");
355
	tberror("Error inserting new apt_profile_versions record!");
356 357 358 359 360 361
	return undef;
    }
    DBQueryWarn("unlock tables");
    return Lookup($class, $pid, $argref->{'name'});
}

362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
#
# Create a new version of a profile.
#
sub NewVersion($$)
{
    my ($self, $creator) = @_;
    my $profileid   = $self->profileid();
    my $version     = $self->version();
    my $uid         = $creator->uid();
    my $uid_idx     = $creator->uid_idx();

    DBQueryWarn("lock tables apt_profiles write, ".
		"            apt_profile_versions write, ".
		"            apt_profile_versions as v write")
	or return undef;

    #
    # This might not be the head version, so have to find the
    # current max.
    #
    my $query_result =
	DBQueryWarn("select max(version) from apt_profile_versions ".
		    "where profileid='$profileid'");
    goto bad
	if (!$query_result || !$query_result->numrows);

    my ($newvers) = $query_result->fetchrow_array() + 1;

    #
    # Insert new version. The "current" version becomes this one.
    #
    goto bad
	if (! DBQueryWarn("insert into apt_profile_versions ".
			  "  (name,profileid,version,pid,pid_idx, ".
			  "   creator,creator_idx,created,uuid, ".
397 398
			  "   parent_profileid,parent_version,rspec, ".
			  "   script,paramdefs) ".
399 400
			  "select name,profileid,'$newvers',pid,pid_idx, ".
			  "  '$uid','$uid_idx',now(),uuid(),parent_profileid, ".
401
			  "  '$version',rspec,script,paramdefs ".
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
			  "from apt_profile_versions as v ".
			  "where v.profileid='$profileid' and ".
			  "      v.version='$version'"));
    if (! DBQueryWarn("update apt_profiles set version=$newvers ".
		      "where profileid='$profileid'")) {
	DBQueryWarn("delete from apt_profile_versions ".
		    "where profileid='$profileid' and version='$version'");
	goto bad;
    }
    DBQueryWarn("unlock tables");
    return APT_Profile->Lookup($profileid, $newvers);
  bad:
    DBQueryWarn("unlock tables");
    return undef;
}

Leigh B Stoller's avatar
Leigh B Stoller committed
418 419 420 421 422 423 424 425 426
#
# Stringify for output.
#
sub Stringify($)
{
    my ($self) = @_;
    
    my $pid       = $self->pid();
    my $name      = $self->name();
427
    my $version   = $self->version();
Leigh B Stoller's avatar
Leigh B Stoller committed
428

429
    return "[Profile: $pid,$name:$version]";
Leigh B Stoller's avatar
Leigh B Stoller committed
430 431
}

432 433 434
#
# Perform some updates ...
#
435
sub UpdateVersion($$)
436 437 438 439 440 441 442
{
    my ($self, $argref) = @_;

    # Must be a real reference. 
    return -1
	if (! ref($self));

443 444
    my $profileid = $self->profileid();
    my $version   = $self->version();
445

446
    my $query = "update apt_profile_versions set ".
447 448
	join(",", map("$_=" . DBQuoteSpecial($argref->{$_}), keys(%{$argref})));

449
    $query .= " where profileid='$profileid' and version='$version'";
450 451 452 453 454 455 456

    return -1
	if (! DBQueryWarn($query));

    return Refresh($self);
}

457 458 459 460
#
# Perform some updates ...
#
sub UpdateMetaData($$)
461
{
462
    my ($self, $argref) = @_;
463 464 465 466 467

    # Must be a real reference. 
    return -1
	if (! ref($self));

468
    my $profileid = $self->profileid();
469

470 471 472 473
    #
    # This is the only metadata we can update.
    #
    my %mods = ();
474
    foreach my $key ("listed", "shared", "public", "topdog") {
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
	if (exists($argref->{$key})) {
	    $mods{$key} = $argref->{$key};
	}
    }
    my $query = "update apt_profiles set ".
	join(",", map("$_=" . DBQuoteSpecial($mods{$_}), keys(%mods)));

    $query .= " where profileid='$profileid'";

    return -1
	if (! DBQueryWarn($query));

    return Refresh($self);
}

sub Delete($$)
{
    my ($self, $purge) = @_;
    my $profileid = $self->profileid();
494

495 496 497 498 499 500 501 502 503 504 505
    $purge = 0
	if (!defined($purge));
    
    DBQueryWarn("lock tables apt_profiles write, apt_profile_versions write")
	or return -1;

    DBQueryWarn("delete from apt_profiles where profileid='$profileid'")
	or goto bad;
    
    if ($purge) {
	goto bad
506 507
	    if (! DBQueryWarn("delete from apt_profile_versions ".
			      "where profileid='$profileid'"));
508 509 510 511 512 513 514 515 516
    }
    else {
	# Set deleted on all of the versions.
	DBQueryWarn("update apt_profile_versions set ".
		    "    deleted=now(),locked=null,locker_pid=0 ".
		    "where profileid='$profileid'")
	    or goto bad;
    }
    DBQueryWarn("unlock tables");
517
    return 0;
518 519 520 521

  bad:
    DBQueryWarn("unlock tables");
    return -1;
522 523
}

524
#
525 526
# Delete a profile version, only allow it if it is the highest
# numbered version.
527
#
528
sub DeleteVersion($)
529 530 531
{
    my ($self) = @_;

532 533
    DBQueryWarn("lock tables apt_profile_versions write, apt_profiles write")
	or return -1;
534

535 536
    my $profileid = $self->profileid();
    my $version   = $self->version();
537

538 539 540 541 542 543 544 545
    #
    # Only the "head" version can be deleted
    #
    my $query_result =
	DBQueryWarn("select max(version) from apt_profile_versions ".
		    "where profileid='$profileid'");
    goto bad
	if (!$query_result || !$query_result->numrows);
546

547 548 549 550 551 552 553 554 555 556 557 558 559 560
    my ($head) = $query_result->fetchrow_array();
    if ($head != $version) {
	print STDERR "Profile::DeleteVersion: not the head version of $self\n";
	goto bad;
    }
    goto bad
	if (!DBQueryWarn("delete from apt_profile_versions ".
			 "where profileid='$profileid' and ".
			 "      version='$version'"));
    goto bad
	if (!DBQueryWarn("update apt_profiles set version=version-1 ".
			 "where profileid='$profileid' and ".
			 "      version='$version'"));
    DBQueryWarn("unlock tables");
561
    return 0;
562 563 564
  bad:
    DBQueryWarn("unlock tables");
    return -1;
565 566
}

567 568 569 570
#
# Condomize a profile rspec by inserting the necessary firewall section
# to each of the nodes.
#
571
sub CheckFirewall($$)
572
{
573
    my ($self, $condomize) = @_;
574 575 576 577 578 579 580 581 582 583 584

    # Must be a real reference. 
    return -1
	if (! ref($self));

    my $rspec = GeniXML::Parse($self->rspec());
    if (! defined($rspec)) {
	print STDERR "Could not parse rspec\n";
	return undef;
    }
    foreach my $ref (GeniXML::FindNodes("n:node", $rspec)->get_nodelist()) {
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
	my @routable_control_ip =
	    GeniXML::FindNodesNS("n:routable_control_ip",
				 $ref,
				 $GeniXML::EMULAB_NS)->get_nodelist();
	my $virtualization_type = GeniXML::GetVirtualizationSubtype($ref);
	#
	# If a XEN container but not a routable IP, then use the basic
	# rules instead of closed, so that ssh is allowed in on the
	# alternate port. That is the only real difference between basic
	# and closed.
	#
	my $style = "closed";
	if (defined($virtualization_type) &&
	    $virtualization_type eq "emulab-xen" && !@routable_control_ip) {
	    $style = "basic";
	}
	
602 603 604 605 606 607 608
	if ($condomize) {
	    #
	    # No settings is easy; wrap it tight.
	    #
	    if (!GeniXML::HasFirewallSettings($ref)) {
		my $firewall = GeniXML::AddElement("firewall", $ref,
						   $GeniXML::EMULAB_NS);
609
		GeniXML::SetText("style", $firewall, $style);
610 611 612 613 614 615 616 617 618
		next;
	    }
	    #
	    # Make sure the existing section has a reasonable setting.
	    #
	    my $settings = GeniXML::FindNodesNS("n:firewall", $ref,
						$GeniXML::EMULAB_NS)->pop();
	    my $style = GeniXML::GetText("style", $settings);
	    if (!defined($style) || $style ne "basic" || $style ne "closed") {
619
		GeniXML::SetText("style", $settings, $style);
620
	    }
621 622
	}
	#
623 624
	# Quick pass over the exceptions to see if we need to substitute
	# the callers IP address.
625
	#
626 627 628 629 630 631
	foreach my $exception (GeniXML::FindNodesNS("n:firewall/n:exception",
				$ref, $GeniXML::EMULAB_NS)->get_nodelist()) {
	    my $ip = GeniXML::GetText("ip", $exception);
	    if (defined($ip) && $ip eq "myip" && exists($ENV{'REMOTE_ADDR'})) {
		GeniXML::SetText("ip", $exception, $ENV{'REMOTE_ADDR'});
	    }
632 633 634 635
	}
    }
    return GeniXML::Serialize($rspec);
}
Leigh B Stoller's avatar
Leigh B Stoller committed
636 637 638 639 640 641 642

#
# Lock and Unlock
#
sub Lock($)
{
    my ($self) = @_;
643
    my $profileid = $self->profileid();
Leigh B Stoller's avatar
Leigh B Stoller committed
644 645 646 647 648

    return -1
	if (!DBQueryWarn("lock tables apt_profiles write"));

    my $query_result =
649 650
	DBQueryWarn("update apt_profiles set locked=now(),locker_pid='$PID' " .
		    "where profileid='$profileid' and locked is null");
Leigh B Stoller's avatar
Leigh B Stoller committed
651 652 653 654 655 656 657

    if (! $query_result ||
	$query_result->numrows == 0) {
	DBQueryWarn("unlock tables");
	return -1;
    }
    DBQueryWarn("unlock tables");
658
    $self->{'DBROW'}->{'locked'} = time();
Leigh B Stoller's avatar
Leigh B Stoller committed
659 660 661 662 663 664
    return 0;
}

sub Unlock($)
{
    my ($self) = @_;
665
    my $profileid = $self->profileid();
Leigh B Stoller's avatar
Leigh B Stoller committed
666 667

    return -1
668 669 670 671 672 673 674 675 676 677 678 679 680
	if (! DBQueryWarn("update apt_profiles set ".
			  "   locked=null,locker_pid=0 ".
			  "where profileid='$profileid'"));
    
    $self->{'DBROW'}->{'locked'} = 0;
    return 0;
}

#
# Update the disk image inside a single node profile. 
#
sub UpdateDiskImage($$)
{
681
    my ($self, $image_url) = @_;
682 683 684 685 686 687
    my $rspec = GeniXML::Parse($self->rspec());
    if (! defined($rspec)) {
	print STDERR "UpdateDiskImage: Could not parse rspec\n";
	return -1;
    }
    my ($node) = GeniXML::FindNodes("n:node", $rspec)->get_nodelist();
688
    GeniXML::SetDiskImage($node, $image_url);
689 690 691 692 693 694 695
    if ($self->UpdateVersion({"rspec" => GeniXML::Serialize($rspec)})) {
	print STDERR "UpdateDiskImage: Could not update rspec\n";
	return -1;
    }
    return 0;
}

696 697 698 699 700 701 702
#
# We need to convert from using URNs to using URLs for disk images,
# since we want to support choosing the backend. This list is the
# list as of the conversion, at the APT. Before we instantiate, look
# at the rspec and update the URNs to URLs based on this list.
#
my %APTImages = (
703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:demo-profile' => 'https://www.apt.emulab.net/image_metadata.php?uuid=39383c39-7b2f-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:docker' => 'https://www.apt.emulab.net/image_metadata.php?uuid=5ae53ff8-7b2f-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:docker-running-env' => 'https://www.apt.emulab.net/image_metadata.php?uuid=a1317423-7b2f-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:docker-running-env-02' => 'https://www.apt.emulab.net/image_metadata.php?uuid=f30e8657-7b2f-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:Docker1' => 'https://www.apt.emulab.net/image_metadata.php?uuid=31d9f5c1-7b30-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:ESA' => 'https://www.apt.emulab.net/image_metadata.php?uuid=72d94622-7b35-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:firstclone' => 'https://www.apt.emulab.net/image_metadata.php?uuid=d49e30a8-7b31-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:FPTaylorVM' => 'https://www.apt.emulab.net/image_metadata.php?uuid=f2e99dbe-7b31-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:FrequentDirection' => 'https://www.apt.emulab.net/image_metadata.php?uuid=1c6b4e98-7b32-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:geni-lib' => 'https://www.apt.emulab.net/image_metadata.php?uuid=441fc279-7b32-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:gimiovsomf6' => 'https://www.apt.emulab.net/image_metadata.php?uuid=626184be-7b32-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:hadoop' => 'https://www.apt.emulab.net/image_metadata.php?uuid=ffa8f859-3524-11e4-8944-81966d62745f',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:hg' => 'https://www.apt.emulab.net/image_metadata.php?uuid=91dcd7d5-7b32-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:imageusage' => 'https://www.apt.emulab.net/image_metadata.php?uuid=adc61bad-7b32-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:Iperf' => 'https://www.apt.emulab.net/image_metadata.php?uuid=d0c8aba4-7b32-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:labwikiXORP' => 'https://www.apt.emulab.net/image_metadata.php?uuid=eee5f68d-7b32-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:myapt' => 'https://www.apt.emulab.net/image_metadata.php?uuid=13baa069-7b33-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:myclone' => 'https://www.apt.emulab.net/image_metadata.php?uuid=301d4978-7b33-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:myimage' => 'https://www.apt.emulab.net/image_metadata.php?uuid=03a342be-7b2c-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:myvm' => 'https://www.apt.emulab.net/image_metadata.php?uuid=6334cf1c-7a59-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:myvm2' => 'https://www.apt.emulab.net/image_metadata.php?uuid=aa8b3638-7a79-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:ricci-cav-2014' => 'https://www.apt.emulab.net/image_metadata.php?uuid=b3d6f6f7-7b35-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:rob-profile' => 'https://www.apt.emulab.net/image_metadata.php?uuid=31278fc7-7b34-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:robtestimage' => 'https://www.apt.emulab.net/image_metadata.php?uuid=4d43d6bb-7b34-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:singlenodelime' => 'https://www.apt.emulab.net/image_metadata.php?uuid=60905cf9-7b34-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:singlenodelime-2' => 'https://www.apt.emulab.net/image_metadata.php?uuid=8db24268-7b34-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:singlevmlime' => 'https://www.apt.emulab.net/image_metadata.php?uuid=b192a572-7b34-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:smack-cav2014-test' => 'https://www.apt.emulab.net/image_metadata.php?uuid=a4594b69-7b36-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:ubuntu1204maas' => 'https://www.apt.emulab.net/image_metadata.php?uuid=e532f26d-7b36-11e4-9439-db9edc46fe2c',
    'urn:publicid:IDN+utahddc.geniracks.net+image+emulab-net:Ubuntu1404' => 'https://www.apt.emulab.net/image_metadata.php?uuid=0838c611-7b37-11e4-9439-db9edc46fe2c'
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770
);

sub ConvertDiskImages($)
{
    my ($self) = @_;
    my $modified = 0;

    my $rspec = GeniXML::Parse($self->rspec());
    if (! defined($rspec)) {
	print STDERR "Could not parse rspec\n";
	return -1;
    }
    foreach my $ref (GeniXML::FindNodes("n:node", $rspec)->get_nodelist()) {
	my $diskref = GeniXML::GetDiskImage($ref);
	next
	    if (!defined($diskref));
	my $imageurn = GeniXML::GetText("name", $diskref);
	next
	    if (!defined($imageurn));
	if (exists($APTImages{$imageurn})) {
	    GeniXML::SetDiskImage($ref, $APTImages{$imageurn});
	    $modified = 1;
	}
    }
    if ($modified) {
	$self->{'DBROW'}->{'rspec'} = GeniXML::Serialize($rspec);

	my $profileid  = $self->profileid();
	my $version    = $self->version();
	my $safe_rspec = DBQuoteSpecial($self->rspec());
	DBQueryWarn("update apt_profile_versions set rspec=$safe_rspec ".
		    "where profileid='$profileid' and version='$version'")
	    if (1);
    }
    return 0;
}

# Total nonsense, to be thrown away.
771
sub CheckNodeConstraints($$$)
772 773 774 775 776 777 778 779 780 781 782 783
{
    my ($self, $iscloudlab, $pmsg) = @_;
    my $cloudwww = "www.utah.cloudlab.us";
    require URI;

    my $rspec = GeniXML::Parse($self->rspec());
    if (! defined($rspec)) {
	print STDERR "Could not parse rspec\n";
	return -1;
    }
    foreach my $ref (GeniXML::FindNodes("n:node", $rspec)->get_nodelist()) {
	my $client_id = GetVirtualId($ref);
784 785 786 787 788 789 790 791
	my $virtualization_type = GeniXML::GetVirtualizationSubtype($ref);

	if (defined($virtualization_type) && $iscloudlab &&
	    $virtualization_type eq "emulab-xen") {
	    $$pmsg = "Node '$client_id' is a XEN VM, which is ".
		"not supported on the Cloudlab cluster";
	    return -1;
	}
792 793 794 795
	my $diskref   = GeniXML::GetDiskImage($ref);
	next
	    if (!defined($diskref));
	my $image_url = GeniXML::GetText("url", $diskref);
796
	my $image_urn = GeniXML::GetText("name", $diskref);
797
	next
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813
	    if (!(defined($image_url) || defined($image_urn)));

	if (defined($image_urn)) {
	    if ($iscloudlab && $image_urn !~ /ARM/) {
		$$pmsg = "Node '$client_id' disk_image will not run ".
		    "on the Cloudlab cluster";
		return -1;
	    }
	    elsif (!$iscloudlab && $image_urn =~ /ARM/) {
		$$pmsg = "Node '$client_id' disk_image will only run ".
		    "on the Cloudlab cluster";
		return -1;
	    }
	}
	next if
	    (!defined($image_url));
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840

	# Get the hostname for the image URL.
	my $uri = URI->new($image_url);
	if (!defined($uri)) {
	    print STDERR "Could not parse $image_url\n";
	    return -1;
	}
	my $image_host = $uri->host();

	if ($iscloudlab) {
	    if ($image_host ne $cloudwww) {
		$$pmsg = "The disk image specified for node '$client_id' ".
		    "will not run on the Cloudlab cluster";
		return -1;
	    }
	}
	else {
	    if ($image_host eq $cloudwww) {
		$$pmsg = "The disk image specified for node '$client_id' ".
		    "will not run on cluster you selected";
		return -1;
	    }
	}
    }
    return 0;
}

841 842 843 844 845
#
# Check blockstores. 
#
sub CheckDatasets($$$)
{
846 847 848
    my ($xml, $ppid, $pmsg) = @_;
    my $pid = $ppid;
    
849 850 851 852 853 854 855 856 857 858
    my $rspec = GeniXML::Parse($xml);
    if (! defined($rspec)) {
	print STDERR "CheckDatasets: Could not parse rspec\n";
	return -1;
    }
    foreach my $ref (GeniXML::FindNodes("n:node", $rspec)->get_nodelist()) {
	foreach my $blockref (GeniXML::FindNodesNS("n:blockstore",
						   $ref,
				   $GeniXML::EMULAB_NS)->get_nodelist()) {
	    my $leaseurn = GeniXML::GetText("persistent", $blockref);
859 860 861 862 863 864 865 866 867

	    #
	    # We only care about datasets here, we let the backend
	    # do the error checking on ephemeral blockstores.
	    #
	    next
		if (!defined($leaseurn));
	    
	    if (!GeniHRN::IsValid($leaseurn)) {
868 869 870
		$$pmsg = "Persistent dataset name is not a valid URN";
		return 1;
	    }
871 872 873 874 875 876 877 878 879 880
	    my ($authority, $type, $id) = GeniHRN::Parse($leaseurn);
	    #
	    # Separate project from name; this is how the rspec specifies
	    # the dataset they want, since it might be in another project
	    #
	    if ($id =~ /^([-\w]+)\/\/(.+)$/) {
		$pid = $1;
		$id  = $2;
	    }
	    
881 882 883 884 885 886 887 888 889
	    #
	    # Not all backends have blockstore support.
	    #
	    if (!APT_Dataset::ValidBlockstoreBackend($authority)) {
		$$pmsg = "Persistent dataset is not on a valid aggregate";
		return 1;
	    }
	    my $dataset = APT_Dataset->Lookup("$pid/$id");
	    if (!defined($dataset)) {
890 891 892 893 894
		$dataset = APT_Dataset->LookupByRemoteURN($leaseurn);
		if (!defined($dataset)) {
		    $$pmsg = "Persistent dataset '$pid/$id' does not exist";
		    return 1;
		}
895
	    }
896 897 898 899 900 901 902 903

	    #
	    # Dataset must already exists on the aggregate. But we have to
	    # make these checks again at instantiation, since the dataset
	    # might be gone, or it might have different permissions
	    # settings.
	    #
	    my ($d_authority) = GeniHRN::Parse($dataset->aggregate_urn());
904 905
	    my ($domain,$subauth) = split(":", $authority);
	    if ($domain ne $d_authority) {
906 907 908
		$$pmsg = "Persistent dataset '$pid/$id' in not on $authority";
		return 1;
	    }
909 910

	    #
911
	    # XXX Need basic frontend permission checks?
912
	    #
913 914 915 916 917
	}
    }
    return 0;
}

918 919 920
sub IsHead($)
{
    my ($self) = @_;
Leigh B Stoller's avatar
Leigh B Stoller committed
921

922
    my $profileid = $self->profileid();
Leigh B Stoller's avatar
Leigh B Stoller committed
923

924 925 926
    my $query_result =
	DBQueryWarn("select max(version) from apt_profile_versions ".
		    "where profileid='$profileid'");
Leigh B Stoller's avatar
Leigh B Stoller committed
927
    return -1
928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946
	if (!$query_result || !$query_result->numrows);

    my ($head) = $query_result->fetchrow_array();
    return ($head == $self->version() ? 1 : 0);
}

#
# Publish a profile. Not sure what this really means yet.
#
sub Publish($)
{
    my ($self) = @_;
    my $profileid = $self->profileid();
    my $version   = $self->version();

    return -1
	if (! DBQueryWarn("update apt_profile_versions set published=now() ".
			  "where profileid='$profileid' and ".
			  "      version='$version'"));
Leigh B Stoller's avatar
Leigh B Stoller committed
947
    
948
    $self->{'DBROW'}->{'published'} = time();
Leigh B Stoller's avatar
Leigh B Stoller committed
949
    return 0;
950

951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972
}

#
# Manage URL
#
sub AdminURL($)
{
    my ($self) = @_;
    my $uuid   = $self->uuid();
    
    require Project;
    
    my $project = Project->Lookup($self->pid_idx());
    return undef
	if (!defined($project));
    
    my $wwwbase = $project->wwwBase();
    $wwwbase .= "/apt"
	if ($project->Brand()->isEmulab());

    return $wwwbase . "/manage_profile.php?uuid=$uuid";
}
Leigh B Stoller's avatar
Leigh B Stoller committed
973

974 975
# _Always_ make sure that this 1 is at the end of the file...
1;