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

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

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

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

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

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

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

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

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

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

144 145 146 147 148 149 150 151 152 153
    my $parser = XML::LibXML->new;
    my $doc;
    eval {
	$doc = $parser->parse_string($rspec);
    };
    if ($@) {
	print STDERR "Failed to parse ticket string: $@\n";
	return undef;
    }

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
157
    my $self = {};
158
    $self->{'rspec'}         = $doc->documentElement();
159
    $self->{'ticket_uuid'}   = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
160
    $self->{'owner_uuid'}    = $owner->uuid();
161
    $self->{'owner_hrn'}     = $owner->hrn();
162
    $self->{'owner_cert'}    = $owner->GetCertificate();
163 164 165
    $self->{'target_uuid'}   = $target->uuid();
    $self->{'target_hrn'}    = $target->hrn();
    $self->{'target_cert'}   = $target->GetCertificate();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
166
    $self->{'seqno'}         = $seqno;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
167 168
    $self->{'ticket_string'} = undef;
    $self->{'component'}     = undef;
169
    $self->{'slice'}         = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
170
    $self->{'stored'}        = 0;	# Stored to the DB.
171
    $self->{'LOCKED'}        = 0;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
172

Leigh B. Stoller's avatar
Leigh B. Stoller committed
173 174 175 176
    #
    # For now, all tickets expire very quickly ...
    #
    $self->{'redeem_before'} =
177
	POSIX::strftime("20%y-%m-%dT%H:%M:%S", localtime(time() + (5*60)));
Leigh B. Stoller's avatar
Leigh B. Stoller committed
178

Leigh B. Stoller's avatar
Leigh B. Stoller committed
179 180 181 182 183 184 185 186
    #
    # 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
187 188 189 190 191 192
    bless($self, $class);

    return $self;
}
# accessors
sub field($$)           { return ($_[0]->{$_[1]}); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
193
sub idx($)		{ return field($_[0], "idx"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
194
sub seqno($)		{ return field($_[0], "seqno"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
195
sub rspec($)		{ return field($_[0], "rspec"); }
196
sub target_uuid($)	{ return field($_[0], "target_uuid"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
197
sub owner_uuid($)	{ return field($_[0], "owner_uuid"); }
198
sub target_hrn($)	{ return field($_[0], "target_hrn"); }
199
sub owner_hrn($)	{ return field($_[0], "owner_hrn"); }
200
sub target_cert($)	{ return field($_[0], "target_cert"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
201
sub owner_cert($)	{ return field($_[0], "owner_cert"); }
202
sub uuid($)		{ return field($_[0], "ticket_uuid"); }
203
sub ticket_uuid($)	{ return field($_[0], "ticket_uuid"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
204
sub ticket($)		{ return field($_[0], "ticket"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
205
sub asString($)		{ return field($_[0], "ticket_string"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
206
sub ticket_string($)	{ return field($_[0], "ticket_string"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
207
sub redeem_before($)	{ return field($_[0], "redeem_before"); }
208
sub expires($)	        { return field($_[0], "expires"); }
209
sub redeemed($   )	{ return field($_[0], "redeemed"); }
210
sub component_uuid($)	{ return field($_[0], "component_uuid"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
211
sub component($)	{ return field($_[0], "component"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
212
sub stored($)		{ return field($_[0], "stored"); }
213 214 215 216
sub slice($)            { return $_[0]->{'slice'}; }
sub slice_uuid($)	{ return $_[0]->slice()->uuid(); }
sub slice_hrn($)	{ return $_[0]->slice()->hrn(); }
sub slice_urn($)	{ return $_[0]->slice()->urn(); }
217
sub LOCKED($)           { return $_[0]->{'LOCKED'}; }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
218

219 220 221 222 223 224 225 226 227

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

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
228 229 230 231 232 233 234 235 236 237 238 239
#
# Stringify for output.
#
sub Stringify($)
{
    my ($self) = @_;
    
    my $idx = $self->idx();
    if (!defined($idx)) {
	my $seqno = $self->seqno();
	$idx = "S$seqno";
    }
240 241
    my $owner_hrn   = $self->owner_hrn();
    my $target_uuid = $self->target_uuid();
242
    
243
    return "[GeniTicket: $idx, owner:$owner_hrn, target_uuid:$target_uuid]";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
244
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
245

Leigh B. Stoller's avatar
Leigh B. Stoller committed
246 247 248 249 250 251 252 253 254 255
#
# 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
256 257 258
#
# Create a ticket object from a signed ticket string.
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
259
sub CreateFromSignedTicket($$;$$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
260
{
Leigh B. Stoller's avatar
Leigh B. Stoller committed
261
    my ($class, $ticket_string, $component, $nosig) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
262

Leigh B. Stoller's avatar
Leigh B. Stoller committed
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
    #
    # 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
283 284
    }

Leigh B. Stoller's avatar
Leigh B. Stoller committed
285 286
    # Use XML::Simple to convert to something we can mess with.
    my $parser = XML::LibXML->new;
287 288 289 290 291 292 293 294
    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
295 296

    # Dig out the rspec.
297 298 299
    my $rspec = GeniXML::FindFirst("//*[local-name()='rspec']",
				   $doc->documentElement());
    if (!defined($rspec)) {
300 301 302 303
	print STDERR "Ticket is missing rspec node\n";
	return undef;
    }
    
304 305
    # Dig out the ticket uuid.
    my ($uuid_node) = $doc->getElementsByTagName("uuid");
306 307 308 309
    if (!defined($uuid_node)) {
	print STDERR "Ticket is missing uuid node\n";
	return undef;
    }
310
    my $ticket_uuid = $uuid_node->to_literal();
311 312 313 314 315 316

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

317 318
    # Dig out the target certificate.
    my ($cert_node) = $doc->getElementsByTagName("target_gid");
319 320 321 322
    if (!defined($cert_node)) {
	print STDERR "Ticket is missing target gid node\n";
	return undef;
    }
323 324
    my $target_certificate =
	GeniCertificate->LoadFromString($cert_node->to_literal());
325 326 327 328
    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
329

330 331 332 333 334 335
    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
336 337 338
	return undef;
    }

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

360
    # Sequence number
Leigh B. Stoller's avatar
Leigh B. Stoller committed
361
    my ($seqno_node) = $doc->getElementsByTagName("serial");
362 363 364 365
    if (!defined($seqno_node)) {
	print STDERR "Ticket is missing seqno node\n";
	return undef;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
366
    my $seqno = $seqno_node->to_literal();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
367

Leigh B. Stoller's avatar
Leigh B. Stoller committed
368 369 370
    if (! ($seqno =~ /^\w+$/)) {
	print STDERR "Invalid sequence number in ticket\n";
	return undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
371 372
    }

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
    # 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
392
    my $self = {};
Leigh B. Stoller's avatar
Leigh B. Stoller committed
393
    $self->{'idx'}           = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
394
    $self->{'rspec'}         = $rspec;
395
    $self->{'ticket_uuid'}   = $ticket_uuid;
396
    $self->{'target_uuid'}   = $target_certificate->uuid();
397
    $self->{'owner_uuid'}    = $owner_certificate->uuid();
398
    $self->{'target_hrn'}    = $target_certificate->hrn();
399
    $self->{'owner_hrn'}     = $owner_certificate->hrn();
400
    $self->{'target_cert'}   = $target_certificate;
401
    $self->{'owner_cert'}    = $owner_certificate;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
402 403
    $self->{'ticket_string'} = $ticket_string;
    $self->{'xmlref'}        = $doc;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
404
    $self->{'component'}     = $component;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
405
    $self->{'seqno'}         = $seqno;
406
    $self->{'expires'}       = $expires;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
407
    $self->{'stored'}        = 0;
408
    $self->{'slice'}         = undef;
409
    $self->{'LOCKED'}        = 0;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
410 411
    
    #
412 413
    # 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
414
    #
Leigh B. Stoller's avatar
Leigh B. Stoller committed
415 416 417 418 419 420 421 422
    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->{'idx'}    = $seqno;
	$self->{'stored'} = 1;
423 424 425 426 427 428 429 430 431 432 433 434 435 436
	#
	# Older tickets might not have the slice set. The CM will
	# set it later though.
	#
	if (defined($row->{'slice_uuid'})) {
	    my $slice_uuid = $row->{'slice_uuid'};
	    my $slice = GeniSlice->Lookup($slice_uuid);
	    if (!defined($slice)) {
		print STDERR
		    "Could not locate slice for ticket from $slice_uuid\n";
		return undef;
	    }
	    $self->{'slice'} = $slice;
	}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
437
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
438 439 440
    bless($self, $class);
    return $self;
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
441

Leigh B. Stoller's avatar
Leigh B. Stoller committed
442
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
443 444
# 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
445
#
446
sub Delete($$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
447
{
448
    my ($self, $flag) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
449 450 451 452

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
453
    if ($self->stored()) {
454 455
	my $idx  = $self->idx();
	my $uuid = $self->ticket_uuid();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
456 457 458
	
	DBQueryWarn("delete from geni_tickets where idx='$idx'")
	    or return -1;
459

460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
	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";
	}
475 476 477 478 479 480 481 482 483
	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
484
	delete($tickets{"$idx"});
Leigh B. Stoller's avatar
Leigh B. Stoller committed
485 486 487 488
    }
    return 0;
}

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 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
#
# 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;
}

539 540
sub SetSlice($$)
{
541
    my ($self, $slice) = @_;
542
    
543
    $self->{'slice'} = $slice;
544 545 546
    return 0;
}

547 548 549
#
# Return the outstanding ticket for a slice.
#
550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
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);
}

568 569 570 571 572 573 574 575 576 577 578 579
#
# Return the rspec in XML for the ticket.
#
sub rspecXML($)
{
    my ($self) = @_;

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

580
    return $self->rspec()->toString();
581 582
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
583 584 585 586 587 588 589 590
#
# 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
591
    return -1
Leigh B. Stoller's avatar
Leigh B. Stoller committed
592 593 594 595 596 597
	if (! ref($self));

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
598
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
599 600 601
# 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
602
#
603
sub Store($;$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
604
{
605
    my ($self, $flags) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
606 607
    my @insert_data  = ();

608 609 610
    $flags = 0
	if (!defined($flags));

611 612 613 614 615
    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();
616 617
    my $expires     = $self->redeem_before() || $self->expires();
    my $slice_uuid  = $self->slice_uuid();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
618

Leigh B. Stoller's avatar
Leigh B. Stoller committed
619 620 621 622 623
    #
    # 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
624 625 626 627
    if (!defined($idx)) {
	$idx = TBGetUniqueIndex('next_ticket', 1);
	$self->{'idx'} = $idx;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
628
    # A locally generated ticket will not have a component. Might change that.
629
    push(@insert_data, "component_uuid='" . $self->component()->uuid() . "'")
Leigh B. Stoller's avatar
Leigh B. Stoller committed
630 631
	if (defined($self->component()));

Leigh B. Stoller's avatar
Leigh B. Stoller committed
632 633 634
    # 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
635
    push(@insert_data, "seqno='$seqno'");
636
    push(@insert_data, "ticket_uuid='$ticket_uuid'");
637
    push(@insert_data, "target_uuid='$target_uuid'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
638
    push(@insert_data, "owner_uuid='$owner_uuid'");
639
    push(@insert_data, "slice_uuid='$slice_uuid'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
640
    push(@insert_data, "redeem_before='$expires'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
641 642 643 644 645 646 647 648
    
    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;

649 650 651 652 653
    if (! ($flags & TICKET_NOSTATS)) {
	if (GeniUsage->NewTicket($self)) {
	    print STDERR
		"GeniTicket::Store: GeniUsage->NewTicket($self) failed\n";
	}
654 655
    }

Leigh B. Stoller's avatar
Leigh B. Stoller committed
656 657
    $tickets{"$idx"}  = $self;
    $self->{'stored'} = 1;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
658 659 660
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
661
#
662
# When we redeem a ticket, we update the history file and delete it.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
663
#
664
sub Redeem($)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
665 666 667 668 669
{
    my ($self) = @_;

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

671 672 673 674 675 676 677 678 679 680 681 682
    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
683

684
    #
685
    # Every ticket/credential gets its own uuid.
686
    #
687
    my $ticket_uuid = NewUUID();
688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711
    $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'};
712
    my $rspec_xml   = $self->rspec()->toString();
713

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

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

792 793
    foreach my $ref (GeniXML::FindNodes("./n:node",
					$self->rspec())->get_nodelist()) {
794 795
	# Skip lan nodes; they are fake.
	next
796
	    if (GeniXML::IsLanNode($ref));
797
	
798 799
	# Skip remote nodes.
	next
800
	    if (!GeniXML::IsLocalNode($ref));
801
	
802
	my $resource_uuid = GeniXML::GetNodeId($ref);
803 804 805 806 807
	if (!defined($resource_uuid)) {
	    print STDERR "No resource id for node in ticket\n";
	    print Dumper($ref);
	    return -1;
	}
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 845 846 847 848 849 850 851

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

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

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

866 867
    foreach my $ref (GeniXML::FindNodes("./n:node",
					$self->rspec())->get_nodelist()) {
868 869
	# Skip lan nodes; they are fake.
	next
870
	    if (GeniXML::IsLanNode($ref));
871
	
872 873
	# Skip remote nodes.
	next
874
	    if (!GeniXML::IsLocalNode($ref));
875
	
876
	my $resource_uuid = GeniXML::GetNodeId($ref);
877 878 879 880 881
	if (!defined($resource_uuid)) {
	    print STDERR "No resource id for node in ticket\n";
	    print Dumper($ref);
	    return -1;
	}
882

Leigh B. Stoller's avatar
Leigh B. Stoller committed
883 884 885 886 887
	my $node = Node->Lookup($resource_uuid);
	next
	    if (!defined($node));

	my $reservation = $node->Reservation();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
888 889
	next
	    if (!defined($reservation));
890 891 892 893

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

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

913 914 915 916 917 918 919 920 921 922 923 924 925 926
#
# 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
927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943
#
# 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;
}

944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968
#
# 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
969 970
# _Always_ make sure that this 1 is at the end of the file...
1;