#!/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; use GeniCredential; use GeniCertificate; use Experiment; use libdb qw(TBGetUniqueIndex); use English; use XML::Simple; use XML::LibXML; use Data::Dumper; use File::Temp qw(tempfile); use overload ('""' => 'Stringify'); # 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"; my $VERIFYCRED = "$TB/sbin/verifygenicred"; my $NFREE = "$TB/bin/nfree"; my $CMCERT = "$TB/etc/genicm.pem"; # 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)); # Mark as coming from the DB. $ticket->{'idx'} = $idx; $ticket->{'stored'} = 1; # Cache. $tickets{"$idx"} = $ticket; return $ticket; } # # Create an unsigned ticket object, to be populated and signed and returned. # sub Create($$$$) { my ($class, $slice, $owner, $rspec) = @_; # Every Ticket gets a new unique index (sequence number). my $seqno = TBGetUniqueIndex('next_ticket', 1); my $self = {}; $self->{'rspec'} = $rspec; $self->{'slice_uuid'} = $slice->uuid(); $self->{'owner_uuid'} = $owner->uuid(); $self->{'slice_cert'} = $slice->cert(); $self->{'owner_cert'} = $owner->cert(); $self->{'seqno'} = $seqno; $self->{'ticket_string'} = undef; $self->{'component'} = undef; $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; bless($self, $class); return $self; } # accessors sub field($$) { return ($_[0]->{$_[1]}); } sub idx($) { return field($_[0], "idx"); } sub seqno($) { return field($_[0], "seqno"); } sub rspec($) { return field($_[0], "rspec"); } sub uuid($) { return field($_[0], "slice_uuid"); } sub slice_uuid($) { return field($_[0], "slice_uuid"); } sub owner_uuid($) { return field($_[0], "owner_uuid"); } sub slice_cert($) { return field($_[0], "slice_cert"); } sub owner_cert($) { return field($_[0], "owner_cert"); } sub ticket($) { return field($_[0], "ticket"); } sub asString($) { return field($_[0], "ticket_string"); } sub ticket_string($) { return field($_[0], "ticket_string"); } sub component($) { return field($_[0], "component"); } 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"; } my $slice_uuid = $self->slice_uuid(); return "[GeniTicket: $idx, slice:$slice_uuid]"; } # # Create a ticket object from a signed ticket string. # sub CreateFromSignedTicket($$;$$) { my ($class, $ticket_string, $component, $nosig) = @_; # # 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); } # 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)); my $rspec = XMLin($rspec_node->toString(), ForceArray => ["node", "link"]); # 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)); my $slice_cert = $uuid_node->to_literal(); my $slice_uuid; GeniCertificate->CertificateInfo($slice_cert, \$slice_uuid) == 0 or return undef; 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)); my $owner_cert = $uuid_node->to_literal(); my $owner_uuid; GeniCertificate->CertificateInfo($owner_cert, \$owner_uuid) == 0 or return undef; if (! ($owner_uuid =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/)) { print STDERR "Invalid owner_uuid in ticket\n"; return undef; } my ($seqno_node) = $doc->getElementsByTagName("serial"); return undef if (!defined($seqno_node)); my $seqno = $seqno_node->to_literal(); if (! ($seqno =~ /^\w+$/)) { print STDERR "Invalid sequence number in ticket\n"; return undef; } my $self = {}; $self->{'idx'} = undef; $self->{'rspec'} = $rspec; $self->{'slice_uuid'} = $slice_uuid; $self->{'owner_uuid'} = $owner_uuid; $self->{'slice_cert'} = $slice_cert; $self->{'owner_cert'} = $owner_cert; $self->{'ticket_string'} = $ticket_string; $self->{'xmlref'} = $doc; $self->{'component'} = $component; $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; } bless($self, $class); return $self; } # # Might have to delete this from the DB, as with an error handing out # a ticket. # sub Delete($) { my ($self) = @_; return -1 if (! ref($self)); if ($self->stored()) { my $idx = $self->idx(); DBQueryWarn("delete from geni_tickets where idx='$idx'") or return -1; delete($tickets{"$idx"}); } return 0; } # # 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) = @_; return -1 if (! ref($self)); $self->{'count'} = $count; return 0; } # # 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. # sub Store($) { my ($self) = @_; my @insert_data = (); my $idx = $self->idx(); my $seqno = $self->seqno(); my $slice_uuid = $self->slice_uuid(); my $owner_uuid = $self->owner_uuid(); # # 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())); # Now tack on other stuff we need. push(@insert_data, "created=now()"); push(@insert_data, "idx='$idx'"); push(@insert_data, "seqno='$seqno'"); 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; $tickets{"$idx"} = $self; $self->{'stored'} = 1; return 0; } # # Sign the ticket before returning it. We capture the output, which is # in XML. # sub Sign($) { my ($self) = @_; return -1 if (!ref($self)); my $idx = $self->seqno(); my $slice_cert = $self->slice_cert(); my $owner_cert = $self->owner_cert(); my $rspec_xml = XMLout($self->rspec(), "NoAttr" => 1); $rspec_xml =~ s/opt\>/rspec\>/g; # # Create a template xml file to sign. # my $template = "\n". "\n". " ticket\n". " $idx\n". " $owner_cert\n". " $slice_cert\n". " \n". " 1\n". " $rspec_xml\n". " \n". "\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. # if (! open(SIGNER, "$SIGNCRED -c $CMCERT $filename |")) { print STDERR "Could not sign $filename\n"; return -1; } my $ticket = ""; while () { $ticket .= $_; } if (!close(SIGNER)) { print STDERR "Could not sign $filename\n"; return -1; } $self->{'ticket_string'} = $ticket; $self->Store() == 0 or return -1; unlink($filename); 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()); } # # Look up a list of tickets for a locally instantiated slice. # Used by the CM. # sub SliceTickets($$$) { my ($class, $slice, $pref) = @_; my $slice_uuid = $slice->uuid(); my @result = (); my $query_result = DBQueryWarn("select idx from geni_tickets ". "where slice_uuid='$slice_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; } # # Release a ticket. Need to release the nodes ... # Used by the CM. # sub Release($) { my ($self) = @_; return undef if (! ref($self)); my $experiment = Experiment->Lookup($self->slice_uuid()); my $pid = $experiment->pid(); my $eid = $experiment->eid(); my @nodeids = (); foreach my $ref (@{$self->rspec()->{'node'}}) { my $resource_uuid = $ref->{'uuid'}; 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("export NORELOAD=1; $NFREE $pid $eid @nodeids"); } $self->Delete(); return 0; } # _Always_ make sure that this 1 is at the end of the file... 1;