GeniTicket.pm.in 24 KB
Newer Older
Leigh B. Stoller's avatar
Leigh B. Stoller committed
1 2
#!/usr/bin/perl -wT
#
3
# GENIPUBLIC-COPYRIGHT
4
# Copyright (c) 2008-2009 University of Utah and the Flux Group.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
5 6 7 8 9 10 11 12 13 14 15 16
# All rights reserved.
#
package GeniTicket;

#
# Some simple ticket stuff.
#
use strict;
use Exporter;
use vars qw(@ISA @EXPORT);

@ISA    = "Exporter";
17
@EXPORT = qw (TICKET_PURGED TICKET_EXPIRED TICKET_REDEEMED
18
	      TICKET_RELEASED TICKET_DELETED TICKET_NOSTATS);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
19 20

use GeniDB;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
21
use GeniCredential;
22
use GeniCertificate;
23 24
use emutil qw(TBGetUniqueIndex);
use GeniUtil;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
25
use English;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
26 27
use XML::Simple;
use XML::LibXML;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
28
use Data::Dumper;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
29 30 31
use Date::Parse;
use POSIX qw(strftime);
use Time::Local;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
32
use File::Temp qw(tempfile);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
33
use overload ('""' => 'Stringify');
Leigh B. Stoller's avatar
Leigh B. Stoller committed
34 35 36 37 38 39 40 41 42

# Configure variables
my $TB		   = "@prefix@";
my $TBOPS          = "@TBOPSEMAIL@";
my $TBAPPROVAL     = "@TBAPPROVALEMAIL@";
my $TBAUDIT   	   = "@TBAUDITEMAIL@";
my $BOSSNODE       = "@BOSSNODE@";
my $OURDOMAIN      = "@OURDOMAIN@";
my $SIGNCRED	   = "$TB/sbin/signgenicred";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
43
my $VERIFYCRED	   = "$TB/sbin/verifygenicred";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
44
my $NFREE	   = "$TB/bin/nfree";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
45
my $CMCERT	   = "$TB/etc/genicm.pem";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
46

47 48 49 50 51
# Ticket release flags
sub TICKET_PURGED()	{ return 1; }
sub TICKET_REDEEMED()	{ return 2; }
sub TICKET_EXPIRED()	{ return 3; }
sub TICKET_RELEASED()	{ return 4; }
52
sub TICKET_DELETED()	{ return 5; }
53
sub TICKET_NOSTATS()	{ return 0x1; }
54

Leigh B. Stoller's avatar
Leigh B. Stoller committed
55 56
# Cache of tickets.
my %tickets = ();
57 58 59 60 61 62 63 64
BEGIN { use GeniUtil; GeniUtil::AddCache(\%tickets); }

# Do not load this for the Clearinghouse XML server.
BEGIN { 
    if (! defined($main::GENI_ISCLRHOUSE)) {
	require Experiment;
    }
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
65 66 67 68 69 70

#
# Lookup by local idx.
#
sub Lookup($$)
{
71 72 73
    my ($class, $token) = @_;
    my $idx;
    my $query_result;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
74

75 76 77 78 79 80 81 82
    if (GeniHRN::IsValid($token)) {
	return undef if !GeniHRN::Authoritative($token, "@OURDOMAIN@");

	my ($authority, $type, $id) = GeniHRN::Parse($token);
	return undef if $type ne "ticket";
	$idx = $id;
    }
    elsif ($token =~ /^\d+$/) {
83 84 85 86 87 88 89 90 91 92 93 94 95 96
	$idx = $token;
    }
    elsif ($token =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/) {
	$query_result =
	    DBQueryWarn("select idx from geni_tickets ".
			"where ticket_uuid='$token'");
	    return undef
		if (! $query_result || !$query_result->numrows);

	    ($idx) = $query_result->fetchrow_array();
    }
    else {
	return undef;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
97 98 99
    return $tickets{"$idx"}
        if (exists($tickets{"$idx"}));

100
    $query_result =
Leigh B. Stoller's avatar
Leigh B. Stoller committed
101 102 103 104 105 106 107 108 109
	DBQueryWarn("select * from geni_tickets where idx='$idx'");

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

    my $row = $query_result->fetchrow_hashref();

    # Map the component
    my $component;
110 111
    if ($row->{'component_uuid'}) {
	$component = GeniComponent->Lookup($row->{'component_uuid'});
Leigh B. Stoller's avatar
Leigh B. Stoller committed
112 113 114 115 116 117 118 119
	return undef
	    if (!defined($component));
    }
    my $ticket = GeniTicket->CreateFromSignedTicket($row->{'ticket_string'},
						    $component, 1);
    return undef
	if (!defined($ticket));

Leigh B. Stoller's avatar
Leigh B. Stoller committed
120 121 122 123
    # We ignore this in the ticket. In fact, we need to change how we bring
    # tickets back into the system.
    $ticket->{'redeem_before'} = $row->{'redeem_before'};

124 125 126
    # Mark as coming from the DB.
    $ticket->{'idx'}    = $idx;
    $ticket->{'stored'} = 1;
127
    $ticket->{'LOCKED'}   = 0;
128 129

    # Cache.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
130 131 132 133
    $tickets{"$idx"} = $ticket;
    return $ticket;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
134
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
135
# Create an unsigned ticket object, to be populated and signed and returned.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
136
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
137
sub Create($$$$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
138
{
139
    my ($class, $target, $owner, $rspec) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
140

Leigh B. Stoller's avatar
Leigh B. Stoller committed
141 142
    # Every Ticket gets a new unique index (sequence number).
    my $seqno = TBGetUniqueIndex('next_ticket', 1);
143

Leigh B. Stoller's avatar
Leigh B. Stoller committed
144
    my $self = {};
Leigh B. Stoller's avatar
Leigh B. Stoller committed
145
    $self->{'rspec'}         = $rspec;
146
    $self->{'ticket_uuid'}   = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
147
    $self->{'owner_uuid'}    = $owner->uuid();
148
    $self->{'owner_hrn'}     = $owner->hrn();
149
    $self->{'owner_cert'}    = $owner->GetCertificate();
150 151 152
    $self->{'target_uuid'}   = $target->uuid();
    $self->{'target_hrn'}    = $target->hrn();
    $self->{'target_cert'}   = $target->GetCertificate();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
153
    $self->{'seqno'}         = $seqno;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
154 155
    $self->{'ticket_string'} = undef;
    $self->{'component'}     = undef;
156
    $self->{'slice_uuid'}    = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
157
    $self->{'stored'}        = 0;	# Stored to the DB.
158
    $self->{'LOCKED'}        = 0;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
159

Leigh B. Stoller's avatar
Leigh B. Stoller committed
160 161 162 163
    #
    # For now, all tickets expire very quickly ...
    #
    $self->{'redeem_before'} =
164
	POSIX::strftime("20%y-%m-%dT%H:%M:%S", localtime(time() + (5*60)));
Leigh B. Stoller's avatar
Leigh B. Stoller committed
165

Leigh B. Stoller's avatar
Leigh B. Stoller committed
166 167 168 169 170 171 172 173
    #
    # Locally generated tickets need a local DB index, which can be the
    # same as the sequence number. A ticket from a remote component will
    # have it own seqno, and so we will generate a locally valid idx for
    # those when when(if) we store them in the DB.
    #
    $self->{'idx'}           = $seqno;
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
174 175 176 177 178 179
    bless($self, $class);

    return $self;
}
# accessors
sub field($$)           { return ($_[0]->{$_[1]}); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
180
sub idx($)		{ return field($_[0], "idx"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
181
sub seqno($)		{ return field($_[0], "seqno"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
182
sub rspec($)		{ return field($_[0], "rspec"); }
183
sub target_uuid($)	{ return field($_[0], "target_uuid"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
184
sub owner_uuid($)	{ return field($_[0], "owner_uuid"); }
185
sub target_hrn($)	{ return field($_[0], "target_hrn"); }
186
sub owner_hrn($)	{ return field($_[0], "owner_hrn"); }
187
sub target_cert($)	{ return field($_[0], "target_cert"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
188
sub owner_cert($)	{ return field($_[0], "owner_cert"); }
189
sub uuid($)		{ return field($_[0], "ticket_uuid"); }
190
sub ticket_uuid($)	{ return field($_[0], "ticket_uuid"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
191
sub ticket($)		{ return field($_[0], "ticket"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
192
sub asString($)		{ return field($_[0], "ticket_string"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
193
sub ticket_string($)	{ return field($_[0], "ticket_string"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
194
sub redeem_before($)	{ return field($_[0], "redeem_before"); }
195
sub expires($)	        { return field($_[0], "expires"); }
196
sub redeemed($   )	{ return field($_[0], "redeemed"); }
197
sub component_uuid($)	{ return field($_[0], "component_uuid"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
198
sub component($)	{ return field($_[0], "component"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
199
sub stored($)		{ return field($_[0], "stored"); }
200
sub slice_uuid($)	{ return field($_[0], "slice_uuid"); }
201
sub LOCKED($)           { return $_[0]->{'LOCKED'}; }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
202 203 204 205 206 207 208 209 210 211 212 213 214

#
# Stringify for output.
#
sub Stringify($)
{
    my ($self) = @_;
    
    my $idx = $self->idx();
    if (!defined($idx)) {
	my $seqno = $self->seqno();
	$idx = "S$seqno";
    }
215 216
    my $owner_hrn   = $self->owner_hrn();
    my $target_uuid = $self->target_uuid();
217
    
218
    return "[GeniTicket: $idx, owner:$owner_hrn, target_uuid:$target_uuid]";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
219
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
220

Leigh B. Stoller's avatar
Leigh B. Stoller committed
221 222 223 224 225 226 227 228 229 230
#
# Flush from our little cache, as for the expire daemon.
#
sub Flush($)
{
    my ($self) = @_;

    delete($tickets{$self->idx()});
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
231 232 233
#
# Create a ticket object from a signed ticket string.
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
234
sub CreateFromSignedTicket($$;$$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
235
{
Leigh B. Stoller's avatar
Leigh B. Stoller committed
236
    my ($class, $ticket_string, $component, $nosig) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
237

Leigh B. Stoller's avatar
Leigh B. Stoller committed
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    #
    # This flag is used to avoid verifying the signature since I do not
    # really care if the component gives me a bad ticket; I am not using
    # it locally, just passing it back to the component at some point.
    #
    $nosig = 0
	if (!defined($nosig));

    if (! $nosig) {
	my ($fh, $filename) = tempfile(UNLINK => 0);
	return undef
	    if (!defined($fh));
	print $fh $ticket_string;
	close($fh);
	system("$VERIFYCRED $filename");
	if ($?) {
	    print STDERR "Ticket in $filename did not verify\n";
	    return undef;
	}
	unlink($filename);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
258 259
    }

Leigh B. Stoller's avatar
Leigh B. Stoller committed
260 261
    # Use XML::Simple to convert to something we can mess with.
    my $parser = XML::LibXML->new;
262 263 264 265 266 267 268 269
    my $doc;
    eval {
	$doc = $parser->parse_string($ticket_string);
    };
    if ($@) {
	print STDERR "Failed to parse ticket string: $@\n";
	return undef;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
270 271 272

    # Dig out the rspec.
    my ($rspec_node) = $doc->getElementsByTagName("rspec");
273 274 275 276 277
    if (!defined($rspec_node)) {
	print STDERR "Ticket is missing rspec node\n";
	return undef;
    }
    
278 279 280 281 282 283 284 285
    my $rspec =
	eval { XMLin($rspec_node->toString(), KeyAttr => [],
		     ForceArray => ["node", "link", "interface",
				    "interface_ref", "linkendpoints"]) };
    if ($@) {
	print STDERR "XMLin error on ticket rspec: $@\n";
	return undef;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
286

287 288
    # Dig out the ticket uuid.
    my ($uuid_node) = $doc->getElementsByTagName("uuid");
289 290 291 292
    if (!defined($uuid_node)) {
	print STDERR "Ticket is missing uuid node\n";
	return undef;
    }
293
    my $ticket_uuid = $uuid_node->to_literal();
294 295 296 297 298 299

    if (! ($ticket_uuid =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/)) {
	print STDERR "Invalid uuid in ticket\n";
	return undef;
    }

300 301
    # Dig out the target certificate.
    my ($cert_node) = $doc->getElementsByTagName("target_gid");
302 303 304 305
    if (!defined($cert_node)) {
	print STDERR "Ticket is missing target gid node\n";
	return undef;
    }
306 307
    my $target_certificate =
	GeniCertificate->LoadFromString($cert_node->to_literal());
308 309 310 311
    if (!defined($target_certificate)) {
	print STDERR "Could not get target certificate from string\n";
	return undef;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
312

313 314 315 316 317 318
    if (!($target_certificate->uuid() =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/)) {
	print STDERR "Invalid target_uuid in credential\n";
	return undef;
    }
    if (!($target_certificate->hrn() =~ /^[\w\.]+$/)) {
	print STDERR "Invalid hrn in credential\n";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
319 320 321
	return undef;
    }

322 323
    # Dig out the owner certificate.
    ($cert_node) = $doc->getElementsByTagName("owner_gid");
324 325 326 327
    if (!defined($cert_node)) {
	print STDERR "Ticket is missing owner gid node\n";
	return undef;
    }
328 329
    my $owner_certificate =
	GeniCertificate->LoadFromString($cert_node->to_literal());
330 331 332 333
    if (!defined($target_certificate)) {
	print STDERR "Could not get owner certificate from string\n";
	return undef;
    }
334 335 336 337 338 339
    if (!($owner_certificate->uuid() =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/)) {
	print STDERR "Invalid target_uuid in credential\n";
	return undef;
    }
    if (!($owner_certificate->hrn() =~ /^[\w\.]+$/)) {
	print STDERR "Invalid hrn in credential\n";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
340 341 342
	return undef;
    }

343
    # Sequence number
Leigh B. Stoller's avatar
Leigh B. Stoller committed
344
    my ($seqno_node) = $doc->getElementsByTagName("serial");
345 346 347 348
    if (!defined($seqno_node)) {
	print STDERR "Ticket is missing seqno node\n";
	return undef;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
349
    my $seqno = $seqno_node->to_literal();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
350

Leigh B. Stoller's avatar
Leigh B. Stoller committed
351 352 353
    if (! ($seqno =~ /^\w+$/)) {
	print STDERR "Invalid sequence number in ticket\n";
	return undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
354 355
    }

356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
    # Expiration
    my ($expires_node) = $doc->getElementsByTagName("expires");
    if (!defined($expires_node)) {
	print STDERR "Ticket is missing expires node\n";
	return undef;
    }
    my $expires = $expires_node->to_literal();

    if (! ($expires =~ /^[-\w:.\/]+/)) {
	print STDERR "Invalid expires date in ticket\n";
	return undef;
    }
    # Convert to a localtime.
    my $when = timegm(strptime($expires));
    if (!defined($when)) {
	print STDERR "Could not parse expires: '$expires'\n";
	return undef;
    }
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
375
    my $self = {};
Leigh B. Stoller's avatar
Leigh B. Stoller committed
376
    $self->{'idx'}           = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
377
    $self->{'rspec'}         = $rspec;
378
    $self->{'ticket_uuid'}   = $ticket_uuid;
379
    $self->{'target_uuid'}   = $target_certificate->uuid();
380
    $self->{'owner_uuid'}    = $owner_certificate->uuid();
381
    $self->{'target_hrn'}    = $target_certificate->hrn();
382
    $self->{'owner_hrn'}     = $owner_certificate->hrn();
383
    $self->{'target_cert'}   = $target_certificate;
384
    $self->{'owner_cert'}    = $owner_certificate;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
385 386
    $self->{'ticket_string'} = $ticket_string;
    $self->{'xmlref'}        = $doc;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
387
    $self->{'component'}     = $component;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
388
    $self->{'seqno'}         = $seqno;
389
    $self->{'expires'}       = $expires;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
390
    $self->{'stored'}        = 0;
391
    $self->{'slice_uuid'}    = undef;
392
    $self->{'LOCKED'}        = 0;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
393 394
    
    #
395 396
    # We save copies of the tickets we hand out, but delete them
    # when redeemed. If we still have it, mark it.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
397
    #
Leigh B. Stoller's avatar
Leigh B. Stoller committed
398 399 400 401 402 403 404 405 406
    my $query_result =
	DBQueryWarn("select * from geni_tickets where idx='$seqno'");

    if ($query_result && $query_result->numrows) {
	my $row = $query_result->fetchrow_hashref();
	$self->{'redeem_before'} = $row->{'redeem_before'};
	$self->{'slice_uuid'}    = $row->{'slice_uuid'};
	$self->{'idx'}    = $seqno;
	$self->{'stored'} = 1;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
407
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
408 409 410 411
    bless($self, $class);

    return $self;
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
412

Leigh B. Stoller's avatar
Leigh B. Stoller committed
413
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
414 415
# Might have to delete this from the DB, as with an error handing out
# a ticket.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
416
#
417
sub Delete($$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
418
{
419
    my ($self, $flag) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
420 421 422 423

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
424
    if ($self->stored()) {
425 426
	my $idx  = $self->idx();
	my $uuid = $self->ticket_uuid();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
427 428 429
	
	DBQueryWarn("delete from geni_tickets where idx='$idx'")
	    or return -1;
430

431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
	if ($flag == TICKET_PURGED) {
	    GeniUsage->DeleteTicket($self) == 0 
		or print STDERR "GeniTicket::Delete: ".
		    "GeniUsage->DeleteTicket($self) failed\n";
	}
	elsif ($flag == TICKET_RELEASED) {
	    GeniUsage->ReleaseTicket($self) == 0 
		or print STDERR "GeniTicket::Delete: ".
		    "GeniUsage->ReleaseTicket($self) failed\n";
	}
	elsif ($flag == TICKET_EXPIRED) {
	    GeniUsage->ExpireTicket($self) == 0 
		or print STDERR "GeniTicket::Delete: ".
		    "GeniUsage->ExpireTicket($self) failed\n";
	}
446 447 448 449 450 451 452 453 454
	elsif ($flag == TICKET_REDEEMED) {
	    GeniUsage->RedeemTicket($self) == 0 
		or print STDERR "GeniTicket::Delete: ".
		"GeniUsage->RedeemTicket($self) failed\n";
	}
	elsif ($flag == TICKET_DELETED) {
	    # Do nothing for this. Just removing from the tickets table,
	    # but want to leave it in the history.
	}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
455
	delete($tickets{"$idx"});
Leigh B. Stoller's avatar
Leigh B. Stoller committed
456 457 458 459
    }
    return 0;
}

460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509
#
# We lock at a very coarse grain, mostly in the CM. 
#
sub Lock($)
{
    my ($self) = @_;
    my $idx    = $self->idx();

    # We already have it locked.
    return 0
	if ($self->LOCKED());

    # Not in the DB, so does not matter.
    return 0
	if (!$self->stored());

    DBQueryWarn("lock tables geni_tickets write")
	or return -1;

    my $query_result =
	DBQueryWarn("select locked from geni_tickets ".
		    "where idx='$idx' and locked is null");
    if (!$query_result || !$query_result->numrows) {
	DBQueryWarn("unlock tables");
	return 1;
    }
    $query_result =
	DBQueryWarn("update geni_tickets set locked=now() where idx='$idx'");
    DBQueryWarn("unlock tables");

    return 1
	if (!$query_result);
    $self->{'LOCKED'} = $$;
    return 0;
}
sub UnLock($)
{
    my ($self) = @_;
    my $idx    = $self->idx();

    return 1
	if (!$self->LOCKED());

    DBQueryWarn("update geni_tickets set locked=NULL where idx='$idx'")
	or return -1;
    
    $self->{'LOCKED'} = 0;
    return 0;
}

510 511 512 513 514 515 516 517
sub SetSlice($$)
{
    my ($self, $slice_uuid) = @_;
    
    $self->{'slice_uuid'} = $slice_uuid;
    return 0;
}

518 519 520
#
# Return the outstanding ticket for a slice.
#
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
sub SliceTicket($$)
{
    my ($class, $slice) = @_;

    my $slice_uuid = $slice->uuid();

    my $query_result =
	DBQueryWarn("select idx from geni_tickets ".
		    "where slice_uuid='$slice_uuid'");
    return undef
	if (!$query_result);
    return undef
	if ($query_result->numrows != 1);

    my ($idx) = $query_result->fetchrow_array();
    return GeniTicket->Lookup($idx);
}

539 540 541 542 543 544 545 546 547 548 549 550
#
# Return the rspec in XML for the ticket.
#
sub rspecXML($)
{
    my ($self) = @_;

    return undef
	if (! ref($self));
    return undef
	if (!defined($self->rspec()));

551 552 553 554 555 556 557
    my $rspec_xml =
	eval { XMLout($self->rspec(),
		      "NoAttr" => 1, RootName => "rspec") };
    if ($@) {
	print STDERR "XMLout error on rspec: $@\n";
	return undef;
    }
558 559 560
    return $rspec_xml;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
561 562 563 564 565 566 567 568
#
# Populate the ticket with some stuff, which right now is just the
# number of node we are willing to grant.
#
sub Grant($$)
{
    my ($self, $count) = @_;

Leigh B. Stoller's avatar
Leigh B. Stoller committed
569
    return -1
Leigh B. Stoller's avatar
Leigh B. Stoller committed
570 571 572 573 574 575
	if (! ref($self));

    $self->{'count'} = $count;
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
576
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
577 578 579
# Store the given ticket in the DB. We only do this for signed tickets,
# so we have a record of them. We store them on the server and the client
# side.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
580
#
581
sub Store($;$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
582
{
583
    my ($self, $flags) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
584 585
    my @insert_data  = ();

586 587 588
    $flags = 0
	if (!defined($flags));

589 590 591 592 593
    my $idx         = $self->idx();
    my $seqno       = $self->seqno();
    my $target_uuid = $self->target_uuid();
    my $owner_uuid  = $self->owner_uuid();
    my $ticket_uuid = $self->ticket_uuid();
594 595
    my $expires     = $self->redeem_before() || $self->expires();
    my $slice_uuid  = $self->slice_uuid();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
596

Leigh B. Stoller's avatar
Leigh B. Stoller committed
597 598 599 600 601
    #
    # For a locally created/signed ticket, seqno=idx. For a ticket from
    # another component, we have to generate a locally unique idx for
    # the DB insertion.
    #
Leigh B. Stoller's avatar
Leigh B. Stoller committed
602 603 604 605
    if (!defined($idx)) {
	$idx = TBGetUniqueIndex('next_ticket', 1);
	$self->{'idx'} = $idx;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
606
    # A locally generated ticket will not have a component. Might change that.
607
    push(@insert_data, "component_uuid='" . $self->component()->uuid() . "'")
Leigh B. Stoller's avatar
Leigh B. Stoller committed
608 609
	if (defined($self->component()));

Leigh B. Stoller's avatar
Leigh B. Stoller committed
610 611 612
    # Now tack on other stuff we need.
    push(@insert_data, "created=now()");
    push(@insert_data, "idx='$idx'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
613
    push(@insert_data, "seqno='$seqno'");
614
    push(@insert_data, "ticket_uuid='$ticket_uuid'");
615
    push(@insert_data, "target_uuid='$target_uuid'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
616
    push(@insert_data, "owner_uuid='$owner_uuid'");
617
    push(@insert_data, "slice_uuid='$slice_uuid'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
618
    push(@insert_data, "redeem_before='$expires'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
619 620 621 622 623 624 625 626
    
    my $safe_ticket = DBQuoteSpecial($self->ticket_string());
    push(@insert_data, "ticket_string=$safe_ticket");

    # Insert into DB.
    DBQueryWarn("insert into geni_tickets set " . join(",", @insert_data))
	or return -1;

627 628 629 630 631
    if (! ($flags & TICKET_NOSTATS)) {
	if (GeniUsage->NewTicket($self)) {
	    print STDERR
		"GeniTicket::Store: GeniUsage->NewTicket($self) failed\n";
	}
632 633
    }

Leigh B. Stoller's avatar
Leigh B. Stoller committed
634 635
    $tickets{"$idx"}  = $self;
    $self->{'stored'} = 1;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
636 637 638
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
639
#
640
# When we redeem a ticket, we update the history file and delete it.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
641
#
642
sub Redeem($)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
643 644 645 646 647
{
    my ($self) = @_;

    return -1
	if (!ref($self));
Leigh B. Stoller's avatar
Leigh B. Stoller committed
648

649 650 651 652 653 654 655 656 657 658 659 660
    return $self->Delete(TICKET_REDEEMED);
}

#
# Sign the ticket before returning it.
#
sub Sign($)
{
    my ($self) = @_;

    return -1
	if (!ref($self));
Leigh B. Stoller's avatar
Leigh B. Stoller committed
661

662
    #
663
    # Every ticket/credential gets its own uuid.
664
    #
665
    my $ticket_uuid = NewUUID();
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
    $self->{'ticket_uuid'} = GeniUtil::NewUUID();

    $self->RunSigner() == 0
	or return -1;

    return 0;
}

#
# Sign the ticket before returning it. We capture the output, which is
# in XML. 
#
sub RunSigner($$)
{
    my ($self) = @_;

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

    my $idx         = $self->seqno();
    my $expires     = $self->redeem_before();
    my $target_cert = $self->target_cert()->cert();
    my $owner_cert  = $self->owner_cert()->cert();
    my $ticket_uuid = $self->{'ticket_uuid'};
    my $rspec_xml   = $self->rspec();

    # Allow for the rspec to be in XML already.
    if (ref($rspec_xml)) {
694 695 696 697 698 699 700
	$rspec_xml =
	    eval { XMLout($rspec_xml,
			  "NoAttr" => 1, RootName => "rspec") };
	if ($@) {
	    print STDERR "XMLout error on ticket rspec: $@\n";
	    return -1;
	}
701
    }
702

Leigh B. Stoller's avatar
Leigh B. Stoller committed
703 704 705 706
    # Convert to GMT.
    $expires = POSIX::strftime("20%y-%m-%dT%H:%M:%S",
			       gmtime(str2time($expires)));

Leigh B. Stoller's avatar
Leigh B. Stoller committed
707 708 709 710 711
    #
    # Create a template xml file to sign.
    #
    my $template =
	"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
712
	"<credential xml:id=\"ref1\">\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
713
	" <type>ticket</type>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
714
	" <serial>$idx</serial>\n".
715
	" <owner_gid>$owner_cert</owner_gid>\n".
716
	" <target_gid>$target_cert</target_gid>\n".
717
	" <uuid>$ticket_uuid</uuid>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
718
	" <expires>$expires</expires>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
719 720
	" <ticket>\n".
	"  <can_delegate>1</can_delegate>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
721
	"  <redeem_before>$expires</redeem_before>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
722
	"  $rspec_xml\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
723 724 725 726 727 728 729 730 731 732 733 734 735 736
	" </ticket>\n".	
        "</credential>\n";

    my ($fh, $filename) = tempfile(UNLINK => 0);
    return -1
	if (!defined($fh));

    print $fh $template;
    close($fh);

    #
    # Fire up the signer and capture the output. This is the signed ticket
    # that is returned. 
    #
Leigh B. Stoller's avatar
Leigh B. Stoller committed
737
    if (! open(SIGNER, "$SIGNCRED -c $CMCERT $filename |")) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
738 739 740 741 742 743 744
	print STDERR "Could not sign $filename\n";
	return -1;
    }
    my $ticket = "";
    while (<SIGNER>) {
	$ticket .= $_;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
745 746 747 748
    if (!close(SIGNER)) {
	print STDERR "Could not sign $filename\n";
	return -1;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
749
    $self->{'ticket_string'} = $ticket;
750 751 752
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
753 754
#
# Release a ticket. Need to release the nodes ...
755
# Used by the CM.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
756
#
757
sub Release($$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
758
{
759
    my ($self, $flag) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
760

761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781
    return -1
	if (! ref($self));

    $self->ReleaseHolding($flag) == 0
	or return -1;

    # Older tickets do not have this, but handled in ReleaseHolding.
    return 0
	if (!defined($self->slice_uuid()) || $self->slice_uuid() eq "");

    my $experiment = Experiment->Lookup($self->slice_uuid());
    if (!defined($experiment)) {
	$self->Delete($flag);
	return 0;
    }
    my $pid     = $experiment->pid();
    my $eid     = $experiment->eid();
    my @nodeids = ();
    my @nodes   = ();

    foreach my $ref (@{$self->rspec()->{'node'}}) {
782 783 784 785 786 787
	# Skip lan nodes; they are fake.
	next
	    if (exists($ref->{'node_type'}) &&
		exists($ref->{'node_type'}->{'type_name'}) &&
		$ref->{'node_type'}->{'type_name'} eq "lan");
	
788 789 790 791 792 793 794
	# Skip remote nodes.
	my $manager_uuid  = $ref->{'component_manager_uuid'};
	next
	    if (defined($manager_uuid) &&
		!GeniHRN::Equal($manager_uuid, $ENV{'MYURN'}) &&
		$manager_uuid ne $ENV{'MYUUID'});
	
795
	my $resource_uuid = $ref->{'component_uuid'} || $ref->{'uuid'};
796 797 798 799 800
	if (!defined($resource_uuid)) {
	    print STDERR "No resource id for node in ticket\n";
	    print Dumper($ref);
	    return -1;
	}
801 802 803 804 805 806 807 808 809 810 811 812 813 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 841 842 843 844

	# Virtual nodes not created until ticket redeemed.
	my $node = Node->Lookup($resource_uuid);
	next
	    if (!defined($node));

	my $reservation = $node->Reservation();
	next
	    if (!defined($reservation));

	# Watch for duplicates, as in multiple vnodes on a pnode.
	next
	    if (grep {$_ eq $node->node_id()} @nodeids);

	#
	# If the node is still in the experiment and not incorporated,
	# release it. genisliver_idx is not defined until ticket redeemed.
	#
	if ($reservation->SameExperiment($experiment)) {
	    my $restable = $node->ReservedTableEntry();
	    if (defined($restable) &&
		!defined($restable->{'genisliver_idx'})) {
		push(@nodeids, $node->node_id());
		push(@nodes, $node);
	    }
	}
    }
    if (@nodeids) {
	system("export NORELOAD=1; $NFREE -x -q $pid $eid @nodeids");
    }
    foreach my $node (@nodes) {
	$node->Refresh();
    }
    $self->Delete($flag);
    return 0;
}

#
# Retained for backwards compatability.
#
sub ReleaseHolding($$)
{
    my ($self, $flag) = @_;

845
    return -1
Leigh B. Stoller's avatar
Leigh B. Stoller committed
846 847
	if (! ref($self));

848
    my $experiment = Experiment->Lookup("GeniSlices", "reservations");
849 850
    if (!defined($experiment)) {
	#
851
	print STDERR "Could not find Geni reservations experiment!";
852
	return 0;
853
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
854 855 856
    my $pid     = $experiment->pid();
    my $eid     = $experiment->eid();
    my @nodeids = ();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
857
    my @nodes   = ();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
858

859
    foreach my $ref (@{$self->rspec()->{'node'}}) {
860 861 862 863 864 865
	# Skip lan nodes; they are fake.
	next
	    if (exists($ref->{'node_type'}) &&
		exists($ref->{'node_type'}->{'type_name'}) &&
		$ref->{'node_type'}->{'type_name'} eq "lan");
	
866 867 868 869 870 871 872
	# Skip remote nodes.
	my $manager_uuid  = $ref->{'component_manager_uuid'};
	next
	    if (defined($manager_uuid) &&
		!GeniHRN::Equal($manager_uuid, $ENV{'MYURN'}) &&
		$manager_uuid ne $ENV{'MYUUID'});
	
873
	my $resource_uuid = $ref->{'component_uuid'} || $ref->{'uuid'};
874 875 876 877 878
	if (!defined($resource_uuid)) {
	    print STDERR "No resource id for node in ticket\n";
	    print Dumper($ref);
	    return -1;
	}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
879 880 881 882 883
	my $node = Node->Lookup($resource_uuid);
	next
	    if (!defined($node));

	my $reservation = $node->Reservation();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
884 885
	next
	    if (!defined($reservation));
886 887 888 889

	# Watch for duplicates, as in multiple vnodes on a pnode.
	next
	    if (grep {$_ eq $node->node_id()} @nodeids);
890 891 892 893 894

	#
	# If the node is still in the reservations experiment, it
	# is by definition, not in use by a slice. 
	#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
895
	if ($reservation->SameExperiment($experiment)) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
896
	    push(@nodeids, $node->node_id());
Leigh B. Stoller's avatar
Leigh B. Stoller committed
897
	    push(@nodes, $node);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
898 899 900
	}
    }
    if (@nodeids) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
901
	system("export NORELOAD=1; $NFREE -x -q $pid $eid @nodeids");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
902
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
903 904 905
    foreach my $node (@nodes) {
	$node->Refresh();
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
906 907 908
    return 0;
}

909 910 911 912 913 914 915 916 917 918 919 920 921 922
#
# Equality test for two tickets.
#
sub SameTicket($$)
{
    my ($self, $other) = @_;

    # Must be a real reference. 
    return 0
	if (! (ref($self) && ref($other)));

    return $self->idx() == $other->idx();
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939
#
# Check if ticket has expired. Use the DB directly.
#
sub Expired($)
{
    my ($self) = @_;
    my $idx    = $self->idx();

    my $query_result =
	DBQueryWarn("select idx from geni_tickets ".
		    "where idx='$idx' and ".
		    "      (UNIX_TIMESTAMP(now()) > ".
		    "       UNIX_TIMESTAMP(redeem_before))");
    
    return $query_result->numrows;
}

940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964
#
# List all tickets for a user.
#
sub ListUserTickets($$$)
{
    my ($class, $user, $pref) = @_;
    my @result = ();

    my $user_uuid = $user->uuid();
    my $query_result = DBQueryWarn("select idx from geni_tickets ".
				   "where owner_uuid='$user_uuid'");
    return -1
	if (!$query_result);

    while (my ($idx) = $query_result->fetchrow_array()) {
	my $ticket = GeniTicket->Lookup($idx);
	return -1
	    if (!defined($ticket));
	push(@result, $ticket);
    }

    @$pref = @result;
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
965 966
# _Always_ make sure that this 1 is at the end of the file...
1;