#!/usr/bin/perl -wT
#
# GENIPUBLIC-COPYRIGHT
# Copyright (c) 2008-2009 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 (TICKET_PURGED TICKET_EXPIRED TICKET_REDEEMED TICKET_RELEASED);
use GeniDB;
use GeniCredential;
use GeniCertificate;
use emutil qw(TBGetUniqueIndex);
use GeniUtil;
use English;
use XML::Simple;
use XML::LibXML;
use Data::Dumper;
use Date::Parse;
use POSIX qw(strftime);
use Time::Local;
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";
# Ticket release flags
sub TICKET_PURGED() { return 1; }
sub TICKET_REDEEMED() { return 2; }
sub TICKET_EXPIRED() { return 3; }
sub TICKET_RELEASED() { return 4; }
# Cache of tickets.
my %tickets = ();
BEGIN { use GeniUtil; GeniUtil::AddCache(\%tickets); }
# Do not load this for the Clearinghouse XML server.
BEGIN {
if (! defined($main::GENI_ISCLRHOUSE)) {
require Experiment;
}
}
#
# 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_uuid'}) {
$component = GeniComponent->Lookup($row->{'component_uuid'});
return undef
if (!defined($component));
}
my $ticket = GeniTicket->CreateFromSignedTicket($row->{'ticket_string'},
$component, 1);
return undef
if (!defined($ticket));
# 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'};
# Mark as coming from the DB.
$ticket->{'idx'} = $idx;
$ticket->{'stored'} = 1;
# Cache.
$tickets{"$idx"} = $ticket;
return $ticket;
}
#
# Lookup a ticket for a slice. This assumes only a single slice ticket.
#
sub LookupForSlice($$)
{
my ($class, $slice) = @_;
return undef
if (! ref($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 || !$query_result->numrows);
if ($query_result->numrows != 1) {
print STDERR "Too many tickets stored for $slice\n";
return undef;
}
my ($idx) = $query_result->fetchrow_array();
return GeniTicket->Lookup($idx);
}
#
# 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->{'ticket_uuid'} = undef;
$self->{'slice_uuid'} = $slice->uuid();
$self->{'owner_uuid'} = $owner->uuid();
$self->{'slice_hrn'} = $slice->hrn();
$self->{'owner_hrn'} = $owner->hrn();
$self->{'slice_cert'} = $slice->GetCertificate();
$self->{'owner_cert'} = $owner->GetCertificate();
$self->{'seqno'} = $seqno;
$self->{'ticket_string'} = undef;
$self->{'component'} = undef;
$self->{'stored'} = 0; # Stored to the DB.
#
# For now, all tickets expire very quickly ...
#
$self->{'redeem_before'} =
POSIX::strftime("20%y-%m-%dT%H:%M:%S", localtime(time() + (5*60)));
#
# 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 slice_uuid($) { return field($_[0], "slice_uuid"); }
sub target_uuid($) { return field($_[0], "slice_uuid"); }
sub owner_uuid($) { return field($_[0], "owner_uuid"); }
sub slice_hrn($) { return field($_[0], "slice_hrn"); }
sub target_hrn($) { return field($_[0], "slice_hrn"); }
sub owner_hrn($) { return field($_[0], "owner_hrn"); }
sub slice_cert($) { return field($_[0], "slice_cert"); }
sub owner_cert($) { return field($_[0], "owner_cert"); }
sub uuid($) { return field($_[0], "ticket_uuid"); }
sub ticket_uuid($) { return field($_[0], "ticket_uuid"); }
sub ticket($) { return field($_[0], "ticket"); }
sub asString($) { return field($_[0], "ticket_string"); }
sub ticket_string($) { return field($_[0], "ticket_string"); }
sub redeem_before($) { return field($_[0], "redeem_before"); }
sub component_uuid($) { return field($_[0], "component_uuid"); }
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]";
}
#
# Flush from our little cache, as for the expire daemon.
#
sub Flush($)
{
my ($self) = @_;
delete($tickets{$self->idx()});
}
#
# 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;
eval {
$doc = $parser->parse_string($ticket_string);
};
if ($@) {
print STDERR "Failed to parse ticket string: $@\n";
return undef;
}
# 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 ticket uuid.
my ($uuid_node) = $doc->getElementsByTagName("uuid");
return undef
if (!defined($uuid_node));
my $ticket_uuid = $uuid_node->to_literal();
if (! ($ticket_uuid =~ /^\w+\-\w+\-\w+\-\w+\-\w+$/)) {
print STDERR "Invalid uuid in ticket\n";
return undef;
}
# Dig out the target certificate.
my ($cert_node) = $doc->getElementsByTagName("target_gid");
return undef
if (!defined($cert_node));
my $target_certificate =
GeniCertificate->LoadFromString($cert_node->to_literal());
return undef
if (!defined($target_certificate));
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";
return undef;
}
# Dig out the owner certificate.
($cert_node) = $doc->getElementsByTagName("owner_gid");
return undef
if (!defined($cert_node));
my $owner_certificate =
GeniCertificate->LoadFromString($cert_node->to_literal());
return undef
if (!defined($owner_certificate));
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";
return undef;
}
# Sequence number
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->{'ticket_uuid'} = $ticket_uuid;
$self->{'slice_uuid'} = $target_certificate->uuid();
$self->{'owner_uuid'} = $owner_certificate->uuid();
$self->{'slice_hrn'} = $target_certificate->hrn();
$self->{'owner_hrn'} = $owner_certificate->hrn();
$self->{'slice_cert'} = $target_certificate;
$self->{'owner_cert'} = $owner_certificate;
$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;
}
my $row = $query_result->fetchrow_hashref();
$self->{'redeem_before'} = $row->{'redeem_before'};
$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, $flag) = @_;
return -1
if (! ref($self));
if ($self->stored()) {
my $idx = $self->idx();
my $uuid = $self->ticket_uuid();
DBQueryWarn("delete from geni_tickets where idx='$idx'")
or return -1;
if ($flag == TICKET_PURGED) {
GeniUsage->DeleteTicket($self) == 0
or print STDERR "GeniTicket::Delete: ".
"GeniUsage->DeleteTicket($self) failed\n";
}
elsif ($flag == TICKET_REDEEMED) {
GeniUsage->RedeemTicket($self) == 0
or print STDERR "GeniTicket::Delete: ".
"GeniUsage->RedeemTicket($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";
}
delete($tickets{"$idx"});
}
return 0;
}
#
# Return the rspec in XML for the ticket.
#
sub rspecXML($)
{
my ($self) = @_;
return undef
if (! ref($self));
return undef
if (!defined($self->rspec()));
my $rspec_xml = XMLout($self->rspec(), "NoAttr" => 1);
$rspec_xml =~ s/opt\>/rspec\>/g;
return $rspec_xml;
}
#
# 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();
my $ticket_uuid= $self->ticket_uuid();
my $expires = $self->redeem_before();
#
# 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.
#
if (!defined($idx)) {
$idx = TBGetUniqueIndex('next_ticket', 1);
$self->{'idx'} = $idx;
}
# A locally generated ticket will not have a component. Might change that.
push(@insert_data, "component_uuid='" . $self->component()->uuid() . "'")
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, "ticket_uuid='$ticket_uuid'");
push(@insert_data, "slice_uuid='$slice_uuid'");
push(@insert_data, "owner_uuid='$owner_uuid'");
push(@insert_data, "redeem_before='$expires'");
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;
if (GeniUsage->NewTicket($self)) {
print STDERR
"GeniTicket::Store: GeniUsage->NewTicket($self) failed\n";
}
$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 $expires = $self->redeem_before();
my $slice_cert = $self->slice_cert()->cert();
my $owner_cert = $self->owner_cert()->cert();
my $rspec_xml = XMLout($self->rspec(), "NoAttr" => 1);
$rspec_xml =~ s/opt\>/rspec\>/g;
#
# Every ticket/credential gets its own uuid.
#
my $ticket_uuid = NewUUID();
$self->{'ticket_uuid'} = $ticket_uuid;
# Convert to GMT.
$expires = POSIX::strftime("20%y-%m-%dT%H:%M:%S",
gmtime(str2time($expires)));
#
# Create a template xml file to sign.
#
my $template =
"\n".
"\n".
" ticket\n".
" $idx\n".
" $owner_cert\n".
" $slice_cert\n".
" $ticket_uuid\n".
" $expires\n".
" \n".
" 1\n".
" $expires\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, $flag) = @_;
return -1
if (! ref($self));
my $experiment = Experiment->Lookup($self->slice_uuid());
if (!defined($experiment)) {
#
# Cannot be any nodes if no experiment.
#
return $self->Delete($flag);
}
my $pid = $experiment->pid();
my $eid = $experiment->eid();
my @nodeids = ();
my @nodes = ();
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();
next
if (!defined($reservation));
if ($reservation->SameExperiment($experiment)) {
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;
}
#
# 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();
}
#
# 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;
}
# _Always_ make sure that this 1 is at the end of the file...
1;