GeniTicket.pm.in 24.2 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;
25
use GeniHRN;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
26
use English;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
27 28
use XML::Simple;
use XML::LibXML;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
29
use Data::Dumper;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
30 31 32
use Date::Parse;
use POSIX qw(strftime);
use Time::Local;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
33
use File::Temp qw(tempfile);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
34
use overload ('""' => 'Stringify');
Leigh B. Stoller's avatar
Leigh B. Stoller committed
35 36 37 38 39 40 41 42 43

# 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
44
my $VERIFYCRED	   = "$TB/sbin/verifygenicred";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
45
my $NFREE	   = "$TB/bin/nfree";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
46
my $CMCERT	   = "$TB/etc/genicm.pem";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
47

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
56 57
# Cache of tickets.
my %tickets = ();
58 59 60 61 62 63 64 65
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
66 67 68 69 70 71

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

76 77 78 79 80 81 82 83
    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+$/) {
84 85 86 87 88 89 90 91 92 93 94 95 96 97
	$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
98 99 100
    return $tickets{"$idx"}
        if (exists($tickets{"$idx"}));

101
    $query_result =
Leigh B. Stoller's avatar
Leigh B. Stoller committed
102 103 104 105 106 107 108 109 110
	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;
111 112
    if ($row->{'component_uuid'}) {
	$component = GeniComponent->Lookup($row->{'component_uuid'});
Leigh B. Stoller's avatar
Leigh B. Stoller committed
113 114 115 116 117 118 119 120
	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
121 122 123 124
    # 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'};

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

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

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

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

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

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
167 168 169 170 171 172 173 174
    #
    # 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
175 176 177 178 179 180
    bless($self, $class);

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

204 205 206 207 208 209 210 211 212

# Return the URN.
sub urn($)
{
    my ($self) = @_;

    return GeniHRN::Generate("@OURDOMAIN@", "ticket", $self->idx());
}

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
231 232 233 234 235 236 237 238 239 240
#
# 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
241 242 243
#
# Create a ticket object from a signed ticket string.
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
244
sub CreateFromSignedTicket($$;$$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
245
{
Leigh B. Stoller's avatar
Leigh B. Stoller committed
246
    my ($class, $ticket_string, $component, $nosig) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
247

Leigh B. Stoller's avatar
Leigh B. Stoller committed
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
    #
    # 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
268 269
    }

Leigh B. Stoller's avatar
Leigh B. Stoller committed
270 271
    # Use XML::Simple to convert to something we can mess with.
    my $parser = XML::LibXML->new;
272 273 274 275 276 277 278 279
    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
280 281 282

    # Dig out the rspec.
    my ($rspec_node) = $doc->getElementsByTagName("rspec");
283 284 285 286 287
    if (!defined($rspec_node)) {
	print STDERR "Ticket is missing rspec node\n";
	return undef;
    }
    
288 289 290 291 292 293 294 295
    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
296

297 298
    # Dig out the ticket uuid.
    my ($uuid_node) = $doc->getElementsByTagName("uuid");
299 300 301 302
    if (!defined($uuid_node)) {
	print STDERR "Ticket is missing uuid node\n";
	return undef;
    }
303
    my $ticket_uuid = $uuid_node->to_literal();
304 305 306 307 308 309

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

310 311
    # Dig out the target certificate.
    my ($cert_node) = $doc->getElementsByTagName("target_gid");
312 313 314 315
    if (!defined($cert_node)) {
	print STDERR "Ticket is missing target gid node\n";
	return undef;
    }
316 317
    my $target_certificate =
	GeniCertificate->LoadFromString($cert_node->to_literal());
318 319 320 321
    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
322

323 324 325 326 327 328
    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
329 330 331
	return undef;
    }

332 333
    # Dig out the owner certificate.
    ($cert_node) = $doc->getElementsByTagName("owner_gid");
334 335 336 337
    if (!defined($cert_node)) {
	print STDERR "Ticket is missing owner gid node\n";
	return undef;
    }
338 339
    my $owner_certificate =
	GeniCertificate->LoadFromString($cert_node->to_literal());
340 341 342 343
    if (!defined($target_certificate)) {
	print STDERR "Could not get owner certificate from string\n";
	return undef;
    }
344 345 346 347 348 349
    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
350 351 352
	return undef;
    }

353
    # Sequence number
Leigh B. Stoller's avatar
Leigh B. Stoller committed
354
    my ($seqno_node) = $doc->getElementsByTagName("serial");
355 356 357 358
    if (!defined($seqno_node)) {
	print STDERR "Ticket is missing seqno node\n";
	return undef;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
359
    my $seqno = $seqno_node->to_literal();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
360

Leigh B. Stoller's avatar
Leigh B. Stoller committed
361 362 363
    if (! ($seqno =~ /^\w+$/)) {
	print STDERR "Invalid sequence number in ticket\n";
	return undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
364 365
    }

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
    # 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
385
    my $self = {};
Leigh B. Stoller's avatar
Leigh B. Stoller committed
386
    $self->{'idx'}           = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
387
    $self->{'rspec'}         = $rspec;
388
    $self->{'ticket_uuid'}   = $ticket_uuid;
389
    $self->{'target_uuid'}   = $target_certificate->uuid();
390
    $self->{'owner_uuid'}    = $owner_certificate->uuid();
391
    $self->{'target_hrn'}    = $target_certificate->hrn();
392
    $self->{'owner_hrn'}     = $owner_certificate->hrn();
393
    $self->{'target_cert'}   = $target_certificate;
394
    $self->{'owner_cert'}    = $owner_certificate;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
395 396
    $self->{'ticket_string'} = $ticket_string;
    $self->{'xmlref'}        = $doc;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
397
    $self->{'component'}     = $component;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
398
    $self->{'seqno'}         = $seqno;
399
    $self->{'expires'}       = $expires;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
400
    $self->{'stored'}        = 0;
401
    $self->{'slice_uuid'}    = undef;
402
    $self->{'LOCKED'}        = 0;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
403 404
    
    #
405 406
    # 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
407
    #
Leigh B. Stoller's avatar
Leigh B. Stoller committed
408 409 410 411 412 413 414 415 416
    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
417
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
418 419 420 421
    bless($self, $class);

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
423
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
424 425
# 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
426
#
427
sub Delete($$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
428
{
429
    my ($self, $flag) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
430 431 432 433

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
434
    if ($self->stored()) {
435 436
	my $idx  = $self->idx();
	my $uuid = $self->ticket_uuid();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
437 438 439
	
	DBQueryWarn("delete from geni_tickets where idx='$idx'")
	    or return -1;
440

441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
	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";
	}
456 457 458 459 460 461 462 463 464
	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
465
	delete($tickets{"$idx"});
Leigh B. Stoller's avatar
Leigh B. Stoller committed
466 467 468 469
    }
    return 0;
}

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 510 511 512 513 514 515 516 517 518 519
#
# 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;
}

520 521 522 523 524 525 526 527
sub SetSlice($$)
{
    my ($self, $slice_uuid) = @_;
    
    $self->{'slice_uuid'} = $slice_uuid;
    return 0;
}

528 529 530
#
# Return the outstanding ticket for a slice.
#
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
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);
}

549 550 551 552 553 554 555 556 557 558 559 560
#
# Return the rspec in XML for the ticket.
#
sub rspecXML($)
{
    my ($self) = @_;

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

561 562 563 564 565 566 567
    my $rspec_xml =
	eval { XMLout($self->rspec(),
		      "NoAttr" => 1, RootName => "rspec") };
    if ($@) {
	print STDERR "XMLout error on rspec: $@\n";
	return undef;
    }
568 569 570
    return $rspec_xml;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
571 572 573 574 575 576 577 578
#
# 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
579
    return -1
Leigh B. Stoller's avatar
Leigh B. Stoller committed
580 581 582 583 584 585
	if (! ref($self));

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
586
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
587 588 589
# 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
590
#
591
sub Store($;$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
592
{
593
    my ($self, $flags) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
594 595
    my @insert_data  = ();

596 597 598
    $flags = 0
	if (!defined($flags));

599 600 601 602 603
    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();
604 605
    my $expires     = $self->redeem_before() || $self->expires();
    my $slice_uuid  = $self->slice_uuid();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
606

Leigh B. Stoller's avatar
Leigh B. Stoller committed
607 608 609 610 611
    #
    # 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
612 613 614 615
    if (!defined($idx)) {
	$idx = TBGetUniqueIndex('next_ticket', 1);
	$self->{'idx'} = $idx;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
616
    # A locally generated ticket will not have a component. Might change that.
617
    push(@insert_data, "component_uuid='" . $self->component()->uuid() . "'")
Leigh B. Stoller's avatar
Leigh B. Stoller committed
618 619
	if (defined($self->component()));

Leigh B. Stoller's avatar
Leigh B. Stoller committed
620 621 622
    # 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
623
    push(@insert_data, "seqno='$seqno'");
624
    push(@insert_data, "ticket_uuid='$ticket_uuid'");
625
    push(@insert_data, "target_uuid='$target_uuid'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
626
    push(@insert_data, "owner_uuid='$owner_uuid'");
627
    push(@insert_data, "slice_uuid='$slice_uuid'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
628
    push(@insert_data, "redeem_before='$expires'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
629 630 631 632 633 634 635 636
    
    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;

637 638 639 640 641
    if (! ($flags & TICKET_NOSTATS)) {
	if (GeniUsage->NewTicket($self)) {
	    print STDERR
		"GeniTicket::Store: GeniUsage->NewTicket($self) failed\n";
	}
642 643
    }

Leigh B. Stoller's avatar
Leigh B. Stoller committed
644 645
    $tickets{"$idx"}  = $self;
    $self->{'stored'} = 1;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
646 647 648
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
649
#
650
# When we redeem a ticket, we update the history file and delete it.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
651
#
652
sub Redeem($)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
653 654 655 656 657
{
    my ($self) = @_;

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

659 660 661 662 663 664 665 666 667 668 669 670
    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
671

672
    #
673
    # Every ticket/credential gets its own uuid.
674
    #
675
    my $ticket_uuid = NewUUID();
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 701 702 703
    $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)) {
704 705 706 707 708 709 710
	$rspec_xml =
	    eval { XMLout($rspec_xml,
			  "NoAttr" => 1, RootName => "rspec") };
	if ($@) {
	    print STDERR "XMLout error on ticket rspec: $@\n";
	    return -1;
	}
711
    }
712

Leigh B. Stoller's avatar
Leigh B. Stoller committed
713 714 715 716
    # 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
717 718 719 720 721
    #
    # 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
722
	"<credential xml:id=\"ref1\">\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
723
	" <type>ticket</type>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
724
	" <serial>$idx</serial>\n".
725
	" <owner_gid>$owner_cert</owner_gid>\n".
726
	" <target_gid>$target_cert</target_gid>\n".
727
	" <uuid>$ticket_uuid</uuid>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
728
	" <expires>$expires</expires>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
729 730
	" <ticket>\n".
	"  <can_delegate>1</can_delegate>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
731
	"  <redeem_before>$expires</redeem_before>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
732
	"  $rspec_xml\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
733 734 735 736 737 738 739 740 741 742 743 744 745 746
	" </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
747
    if (! open(SIGNER, "$SIGNCRED -c $CMCERT $filename |")) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
748 749 750 751 752 753 754
	print STDERR "Could not sign $filename\n";
	return -1;
    }
    my $ticket = "";
    while (<SIGNER>) {
	$ticket .= $_;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
755 756 757 758
    if (!close(SIGNER)) {
	print STDERR "Could not sign $filename\n";
	return -1;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
759
    $self->{'ticket_string'} = $ticket;
760 761 762
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
763 764
#
# Release a ticket. Need to release the nodes ...
765
# Used by the CM.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
766
#
767
sub Release($$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
768
{
769
    my ($self, $flag) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
770

771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
    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'}}) {
792 793 794 795 796 797
	# 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");
	
798 799 800 801 802 803 804
	# 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'});
	
805
	my $resource_uuid = $ref->{'component_uuid'} || $ref->{'uuid'};
806 807 808 809 810
	if (!defined($resource_uuid)) {
	    print STDERR "No resource id for node in ticket\n";
	    print Dumper($ref);
	    return -1;
	}
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 845 846 847 848 849 850 851 852 853 854

	# 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) = @_;

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

858
    my $experiment = Experiment->Lookup("GeniSlices", "reservations");
859 860
    if (!defined($experiment)) {
	#
861
	print STDERR "Could not find Geni reservations experiment!";
862
	return 0;
863
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
864 865 866
    my $pid     = $experiment->pid();
    my $eid     = $experiment->eid();
    my @nodeids = ();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
867
    my @nodes   = ();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
868

869
    foreach my $ref (@{$self->rspec()->{'node'}}) {
870 871 872 873 874 875
	# 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");
	
876 877 878 879 880 881 882
	# 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'});
	
883
	my $resource_uuid = $ref->{'component_uuid'} || $ref->{'uuid'};
884 885 886 887 888
	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
889 890 891 892 893
	my $node = Node->Lookup($resource_uuid);
	next
	    if (!defined($node));

	my $reservation = $node->Reservation();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
894 895
	next
	    if (!defined($reservation));
896 897 898 899

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

	#
	# 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
905
	if ($reservation->SameExperiment($experiment)) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
906
	    push(@nodeids, $node->node_id());
Leigh B. Stoller's avatar
Leigh B. Stoller committed
907
	    push(@nodes, $node);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
908 909 910
	}
    }
    if (@nodeids) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
911
	system("export NORELOAD=1; $NFREE -x -q $pid $eid @nodeids");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
912
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
913 914 915
    foreach my $node (@nodes) {
	$node->Refresh();
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
916 917 918
    return 0;
}

919 920 921 922 923 924 925 926 927 928 929 930 931 932
#
# 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
933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949
#
# 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;
}

950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974
#
# 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
975 976
# _Always_ make sure that this 1 is at the end of the file...
1;