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

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

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

# Must come after package declaration!
use lib '@prefix@/lib';
use GeniDB;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
22
use GeniCredential;
23
use GeniCertificate;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
24
use libtestbed;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
25 26
use Experiment;
use libdb qw(TBGetUniqueIndex);
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 31
use Data::Dumper;
use File::Temp qw(tempfile);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
32
use overload ('""' => 'Stringify');
Leigh B. Stoller's avatar
Leigh B. Stoller committed
33 34 35 36 37 38 39 40 41

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
# Cache of tickets.
my %tickets = ();

#
# Lookup by local idx.
#
sub Lookup($$)
{
    my ($class, $idx) = @_;

    return undef
	if (! ($idx =~ /^\d*$/));
    return $tickets{"$idx"}
        if (exists($tickets{"$idx"}));

    my $query_result =
	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;
    if ($row->{'component_idx'}) {
	$component = GeniComponent->Lookup($row->{'component_idx'});
	return undef
	    if (!defined($component));
    }
    my $ticket = GeniTicket->CreateFromSignedTicket($row->{'ticket_string'},
						    $component, 1);
    return undef
	if (!defined($ticket));

    $tickets{"$idx"} = $ticket;
    return $ticket;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
85
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
86
# Create an unsigned ticket object, to be populated and signed and returned.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
87
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
88
sub Create($$$$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
89
{
Leigh B. Stoller's avatar
Leigh B. Stoller committed
90
    my ($class, $slice, $owner, $rspec) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
91

Leigh B. Stoller's avatar
Leigh B. Stoller committed
92 93 94
    # Every Ticket gets a new unique index (sequence number).
    my $seqno = TBGetUniqueIndex('next_ticket', 1);
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
95
    my $self = {};
Leigh B. Stoller's avatar
Leigh B. Stoller committed
96
    $self->{'rspec'}         = $rspec;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
97 98 99 100
    $self->{'slice_uuid'}    = $slice->uuid();
    $self->{'owner_uuid'}    = $owner->uuid();
    $self->{'slice_cert'}    = $slice->cert();
    $self->{'owner_cert'}    = $owner->cert();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
101
    $self->{'seqno'}         = $seqno;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
102 103
    $self->{'ticket_string'} = undef;
    $self->{'component'}     = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
104 105 106 107 108 109 110 111 112 113
    $self->{'stored'}        = 0;	# Stored to the DB.

    #
    # 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
114 115 116 117 118 119
    bless($self, $class);

    return $self;
}
# accessors
sub field($$)           { return ($_[0]->{$_[1]}); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
120
sub idx($)		{ return field($_[0], "idx"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
121
sub seqno($)		{ return field($_[0], "seqno"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
122
sub rspec($)		{ return field($_[0], "rspec"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
123 124 125
sub uuid($)		{ return field($_[0], "slice_uuid"); }
sub slice_uuid($)	{ return field($_[0], "slice_uuid"); }
sub owner_uuid($)	{ return field($_[0], "owner_uuid"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
126 127
sub slice_cert($)	{ return field($_[0], "slice_cert"); }
sub owner_cert($)	{ return field($_[0], "owner_cert"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
128
sub ticket($)		{ return field($_[0], "ticket"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
129
sub asString($)		{ return field($_[0], "ticket_string"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
130
sub ticket_string($)	{ return field($_[0], "ticket_string"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
131
sub component($)	{ return field($_[0], "component"); }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
sub stored($)		{ return field($_[0], "stored"); }

#
# Stringify for output.
#
sub Stringify($)
{
    my ($self) = @_;
    
    my $idx = $self->idx();
    if (!defined($idx)) {
	my $seqno = $self->seqno();
	$idx = "S$seqno";
    }
    return "[GeniTicket: $idx]";
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
148 149 150 151

#
# Create a ticket object from a signed ticket string.
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
152
sub CreateFromSignedTicket($$;$$)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
153
{
Leigh B. Stoller's avatar
Leigh B. Stoller committed
154
    my ($class, $ticket_string, $component, $nosig) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
155

Leigh B. Stoller's avatar
Leigh B. Stoller committed
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
    #
    # 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
176 177
    }

Leigh B. Stoller's avatar
Leigh B. Stoller committed
178 179 180 181 182 183 184 185
    # Use XML::Simple to convert to something we can mess with.
    my $parser = XML::LibXML->new;
    my $doc    = $parser->parse_string($ticket_string);

    # Dig out the rspec.
    my ($rspec_node) = $doc->getElementsByTagName("rspec");
    return undef
	if (!defined($rspec_node));
186 187
    my $rspec = XMLin($rspec_node->toString(), ForceArray => ["node",
							      "link"]);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
188 189 190 191 192 193

    # Dig out the slice uuid. Locally, I am not sure if we bother to
    # keep slices in the DB (they are in the DB at geni central).
    my ($uuid_node) = $doc->getElementsByTagName("this_uuid");
    return undef
	if (!defined($uuid_node));
Leigh B. Stoller's avatar
Leigh B. Stoller committed
194 195
    my $slice_cert = $uuid_node->to_literal();
    my $slice_uuid;
196
    GeniCertificate->CertificateInfo($slice_cert, \$slice_uuid) == 0
Leigh B. Stoller's avatar
Leigh B. Stoller committed
197
	or return undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
198 199 200 201 202 203 204 205 206 207 208

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

    # Dig out the owner uuid. Locally, I am not sure if we bother to
    # keep users in the DB (they are in the DB at geni central).
    ($uuid_node) = $doc->getElementsByTagName("owner_uuid");
    return undef
	if (!defined($uuid_node));
Leigh B. Stoller's avatar
Leigh B. Stoller committed
209 210
    my $owner_cert = $uuid_node->to_literal();
    my $owner_uuid;
211
    GeniCertificate->CertificateInfo($owner_cert, \$owner_uuid) == 0
Leigh B. Stoller's avatar
Leigh B. Stoller committed
212
	or return undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
213 214 215 216 217 218

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
219 220 221 222
    my ($seqno_node) = $doc->getElementsByTagName("serial");
    return undef
	if (!defined($seqno_node));
    my $seqno = $seqno_node->to_literal();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
223

Leigh B. Stoller's avatar
Leigh B. Stoller committed
224 225 226
    if (! ($seqno =~ /^\w+$/)) {
	print STDERR "Invalid sequence number in ticket\n";
	return undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
227 228
    }

Leigh B. Stoller's avatar
Leigh B. Stoller committed
229
    my $self = {};
Leigh B. Stoller's avatar
Leigh B. Stoller committed
230
    $self->{'idx'}           = undef;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
231 232 233
    $self->{'rspec'}         = $rspec;
    $self->{'slice_uuid'}    = $slice_uuid;
    $self->{'owner_uuid'}    = $owner_uuid;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
234 235
    $self->{'slice_cert'}    = $slice_cert;
    $self->{'owner_cert'}    = $owner_cert;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
236 237
    $self->{'ticket_string'} = $ticket_string;
    $self->{'xmlref'}        = $doc;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
238
    $self->{'component'}     = $component;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
    $self->{'seqno'}         = $seqno;
    $self->{'stored'}        = 0;
    
    #
    # We save copies of the tickets we hand out; lets find that record
    # in the DB, just to verify.
    #
    if (! $nosig) {
	my $query_result =
	    DBQueryWarn("select * from geni_tickets where idx='$seqno'");
	if (!$query_result || !$query_result->numrows) {
	    print STDERR "Could not find the ticket $seqno in the DB\n";
	    return undef;
	}
	$self->{'idx'}    = $seqno;
	$self->{'stored'} = 1;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
256 257 258 259
    bless($self, $class);

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
261
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
262 263
# 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
264 265 266 267 268 269 270 271
#
sub Delete($)
{
    my ($self) = @_;

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
272
    if ($self->stored()) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
273 274 275 276
	my $idx = $self->idx();
	
	DBQueryWarn("delete from geni_tickets where idx='$idx'")
	    or return -1;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
277 278
	
	delete($tickets{"$idx"});
Leigh B. Stoller's avatar
Leigh B. Stoller committed
279 280 281 282
    }
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
283 284 285 286 287 288 289 290
#
# 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
291
    return -1
Leigh B. Stoller's avatar
Leigh B. Stoller committed
292 293 294 295 296 297
	if (! ref($self));

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
298
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
299 300 301
# 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
302
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
303
sub Store($)
Leigh B. Stoller's avatar
Leigh B. Stoller committed
304
{
Leigh B. Stoller's avatar
Leigh B. Stoller committed
305
    my ($self) = @_;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
306 307
    my @insert_data  = ();

Leigh B. Stoller's avatar
Leigh B. Stoller committed
308 309
    my $idx        = $self->idx();
    my $seqno      = $self->seqno();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
310 311 312
    my $slice_uuid = $self->slice_uuid();
    my $owner_uuid = $self->owner_uuid();

Leigh B. Stoller's avatar
Leigh B. Stoller committed
313 314 315 316 317 318 319 320 321 322 323
    #
    # 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.
    #
    $idx = TBGetUniqueIndex('next_ticket', 1)
	if (!defined($idx));
    # A locally generated ticket will not have a component. Might change that.
    push(@insert_data, "component_idx=" . $self->component()->idx())
	if (defined($self->component()));

Leigh B. Stoller's avatar
Leigh B. Stoller committed
324 325 326
    # 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
327
    push(@insert_data, "seqno='$seqno'");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
328 329 330 331 332 333 334 335 336 337
    push(@insert_data, "slice_uuid='$slice_uuid'");
    push(@insert_data, "owner_uuid='$owner_uuid'");
    
    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;

Leigh B. Stoller's avatar
Leigh B. Stoller committed
338 339
    $tickets{"$idx"}  = $self;
    $self->{'stored'} = 1;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
340 341 342
    return 0;
}

Leigh B. Stoller's avatar
Leigh B. Stoller committed
343 344
#
# Sign the ticket before returning it. We capture the output, which is
Leigh B. Stoller's avatar
Leigh B. Stoller committed
345
# in XML. 
Leigh B. Stoller's avatar
Leigh B. Stoller committed
346 347 348 349 350 351 352
#
sub Sign($)
{
    my ($self) = @_;

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

Leigh B. Stoller's avatar
Leigh B. Stoller committed
354
    my $idx        = $self->seqno();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
355 356
    my $slice_cert = $self->slice_cert();
    my $owner_cert = $self->owner_cert();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
357 358
    my $rspec_xml  = XMLout($self->rspec(), "NoAttr" => 1);
    $rspec_xml =~ s/opt\>/rspec\>/g;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
359 360 361 362 363 364

    #
    # 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
365
	"<credential xml:id=\"ref1\">\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
366
	" <type>ticket</type>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
367
	" <serial>$idx</serial>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
368 369
	" <owner_uuid>$owner_cert</owner_uuid>\n".
	" <this_uuid>$slice_cert</this_uuid>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
370 371
	" <ticket>\n".
	"  <can_delegate>1</can_delegate>\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
372
	"  $rspec_xml\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
373 374 375 376 377 378 379 380 381 382 383 384 385 386
	" </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
387
    if (! open(SIGNER, "$SIGNCRED -c $CMCERT $filename |")) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
388 389 390 391 392 393 394
	print STDERR "Could not sign $filename\n";
	return -1;
    }
    my $ticket = "";
    while (<SIGNER>) {
	$ticket .= $_;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
395 396 397 398
    if (!close(SIGNER)) {
	print STDERR "Could not sign $filename\n";
	return -1;
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
399
    $self->{'ticket_string'} = $ticket;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
400

Leigh B. Stoller's avatar
Leigh B. Stoller committed
401
    $self->Store() == 0
Leigh B. Stoller's avatar
Leigh B. Stoller committed
402 403
	or return -1;

Leigh B. Stoller's avatar
Leigh B. Stoller committed
404
    unlink($filename);
Leigh B. Stoller's avatar
Leigh B. Stoller committed
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
    return 0;
}

#
# Get the experiment for the slice this sliver belongs to.
#
sub GetExperiment($)
{
    my ($self) = @_;

    return undef
	if (! ref($self));

    return Experiment->Lookup($self->slice_uuid());
}

#
# Release a ticket. Need to release the nodes ...
#
sub Release($)
{
    my ($self) = @_;

    return undef
	if (! ref($self));

    my $experiment = Experiment->Lookup($self->slice_uuid());
Leigh B. Stoller's avatar
Leigh B. Stoller committed
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
    my $pid     = $experiment->pid();
    my $eid     = $experiment->eid();
    my @nodeids = ();

    foreach my $resource_uuid (keys(%{$self->rspec()->{'node'}})) {
	my $node = Node->Lookup($resource_uuid);
	next
	    if (!defined($node));

	my $reservation = $node->Reservation();
	if (defined($reservation) &&
	    $reservation->SameExperiment($experiment)) {
	    push(@nodeids, $node->node_id());
	}
    }
    if (@nodeids) {
	system("$NFREE $pid $eid @nodeids");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
449 450
    }
    $self->Delete();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
451 452 453
    return 0;
}

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