#!/usr/bin/perl -w
#
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file. If not, see .
#
# }}}
#
package libimageops_docker;
use strict;
use libimageops;
use base qw(libimageops_base);
use libdb;
use libtestbed;
use libtblog;
use User;
use Node;
use English;
use Data::Dumper;
use overload ('""' => 'Stringify');
use File::Temp qw/ :mktemp /;
my $TB = "@prefix@";
my $OURDOMAIN = "@OURDOMAIN@";
my $CREATEDOCKERIMAGE = "/usr/local/bin/create-docker-image";
my $DOCKREGCLI = "/usr/local/bin/docker-registry-cli";
# Prefer dev trees for docker creds, but bail to regular place.
my $DOCKCREDDIR = "$TB/etc/docker";
if (! -d "$TB/etc/docker" && -d "/usr/testbed/etc/docker") {
$DOCKCREDDIR = "/usr/testbed/etc/docker";
}
my $ADMINDOCKREGCLI = "$DOCKREGCLI -C $DOCKCREDDIR/admin-boss.creds" .
" --cert $DOCKCREDDIR/cert.pem --key $DOCKCREDDIR/privkey.pem";
sub New($)
{
my ($class) = @_;
my $self = $class->SUPER::New();
$self->{'HASH'}->{'maxwait'} = 8 * 60;
$self->{'HASH'}->{'idlewait'} = 2 * 60;
$self->{'HASH'}->{'reportwait'} = 1 * 60;
$self->{'HASH'}->{'maximagesize'} = 1 * 1024 ** 3;
bless($self, $class);
return $self;
}
#
# No quotas in Docker-land for now.
#
sub CheckImageQuota($$$$) {
my ($self,$pid,$gid,$user) = @_;
my $rc = 0;
my $msg;
if (wantarray) {
return ($rc,$msg);
}
else {
return $rc;
}
}
sub CreateImageValidateArgs($$$$) {
my ($self,$image,$node,$args) = @_;
my $rc = -1;
my $msg;
#
# Need to know this is a docker-host to tailor method below.
#
if (!$node->isvirtnode()) {
$msg = "$node does not appear to be a virtnode!";
goto out;
}
my $pnode = Node->Lookup($node->phys_nodeid());
my $osimage = OSImage->Lookup($pnode->def_boot_osid());
if (!defined($osimage)) {
$msg = "Could not get OSImage for $pnode (virtnode $node)";
goto errout;
}
if ($osimage->FeatureSupported("docker-host")) {
$args->{'delta'} = $args->{'signature'} = 0;
}
$rc = 0;
out:
tbwarn("$self CreateImageValidateArgs: $msg ($rc)\n")
if ($rc);
if (wantarray) {
return ($rc,$msg);
}
else {
return $rc;
}
}
sub GetLocalRegistryPathForImage($$;$$$) {
my ($self,$image,$registry_ref,$repo_ref,$tag_ref) = @_;
my ($rc,$msg) = (undef,"");
my $registry;
#
# Find the local cluster's registry, if any.
#
if (!TBGetSiteVar("docker/registry",\$registry) || $registry eq "") {
$msg = "no local registry specified in sitevar general/docker_registry";
goto out;
}
#
# Docker registry does not accept uppercase characters in repo names.
#
my $repo = lc($image->pid() . "/" . $image->gid() . "/" .
$image->imagename());
my $tag = $image->version();
$rc = "$registry/$repo:$tag";
$$registry_ref = $registry
if (defined($registry_ref));
$$repo_ref = $repo
if (defined($repo_ref));
$$tag_ref = $tag
if (defined($tag_ref));
out:
if (wantarray) {
return ($rc,$msg);
}
else {
return $rc;
}
}
sub CaptureImage($$$$) {
my ($self,$image,$node,$args) = @_;
my $rc = -1;
my $msg;
my $node_id = $node->node_id();
my $phys_node_id = $node->phys_nodeid();
my $webtask = $args->{'webtask'};
my $this_user = $args->{'user'};
my $imagepid = $args->{'imagepid'} || $image->pid();
my $imagename = $image->imagename();
my ($path,$registry,$repo,$tag);
my $rs = join("",(0..9,"A".."Z","a".."z")[map { rand(62) } (0..7)]);
my $needunlock = 0;
($path,$msg) = $self->GetLocalRegistryPathForImage(
$image,\$registry,\$repo,\$tag);
if (!defined($path)) {
$rc = -1;
goto errout;
}
#
# Before we do anything destructive, we lock the image.
#
if ($image->Lock()) {
$msg = "Image is locked, please try again later ($PID)!";
goto out;
}
$needunlock = 1;
#
# Update the image to point to the repo/tag; change its format.
#
$image->SetFormat("docker");
$image->SetPath($path);
# Save off the "filename".
$args->{'filename'} = $path;
#
# Also update the image to run on type pcvm, so that we can do real
# osloads of this image on pcvms. This should probably be somewhere
# else, but no big deal.
#
$image->SetRunsOnNodeType('pcvm');
#
# We need a temporary user/pass credential for the docker
# login/commit/push on the node to login to our cluster's local
# Docker registry. So create that. We need to remember these to
# delete the token post-imaging.
#
my $experiment = $node->Reservation();
my ($docker_user,$docker_pass);
$docker_user = "exim" . $experiment->idx() . "-$rs";
$docker_pass = TBGenSecretKey();
if (User->AddTokenPassword(
$this_user,$this_user,"docker.registry","image.create",$repo,
$docker_user,$docker_pass,undef,3600,3600,0,1)) {
$msg = "Could not create temporary Docker Registry credentials";
goto out;
}
# XXX continuing... also need to fixup image descriptor creation,
# which is pre-this file.
my $debugarg = "";
if ($self->debug()) {
$debugarg = "-d 10";
}
my $command =
"$CREATEDOCKERIMAGE $debugarg -R $registry -r $repo -t $tag" .
" -u $docker_user -p $docker_pass $node_id";
# Mark webtask
$webtask->status("imaging")
if (defined($webtask));
# Clear the bootlog; see below.
$node->ClearBootLog();
#
# Big hack; we want to tell the node to update the master password
# files. But need to do this in a backwards compatable manner, and
# in way that does not require too much new plumbing. So, just touch
# file in /var/run, the current version of prepare looks for it.
#
my $SAVEUID;
if ($args->{'update_prepare'}) {
$SAVEUID = $UID;
$EUID = $UID = 0;
my $cmd = "$TB/bin/sshtb -n -o ConnectTimeout=10 ".
"-host $node_id touch /var/run/updatemasterpasswdfiles";
print STDERR "About to: '$cmd'\n"
if ($self->debug());
system($cmd);
if ($?) {
$msg = "'$cmd' failed";
goto errout;
}
$EUID = $UID = $SAVEUID;
}
#
# Now execute command and wait. If we have a webtask, capture the
# output of the command so we can spawn another child to process it;
# but do that before suid.
#
my ($fname,$rpid);
if (defined($webtask)) {
$fname = mktemp("/tmp/create-docker-image-XXXXXX");
open(TFD,">$fname");
close(TFD);
$rpid = fork();
if ($rpid == 0) {
$SIG{INT} = sub { exit(0); };
while (!stat($fname)) {
sleep(0.1);
}
open(FD,$fname);
while (1) {
while (my $line = ) {
if ($self->debug()) {
print "CREATEIMAGE: $line";
}
chomp($line);
if ($line =~ /PUSHPROGRESS:\s+(\d+)\s+bytes/) {
$webtask->Refresh();
$webtask->imagesize(int($1)/(1024));
}
}
sleep(0.1);
}
exit(0);
}
}
$SAVEUID = $UID;
$EUID = $UID = 0;
my $result = $self->RunWithSSH($phys_node_id,undef,$command,$fname);
$EUID = $UID = $SAVEUID;
if (defined($webtask)) {
sleep(1);
kill(2,$rpid);
waitpid($rpid,0);
}
User->DeleteTokenPasswords($this_user,$this_user,"docker.registry",
$docker_user,$docker_pass);
# Grab boot log now. Node will reboot and possibly erase it. We should
# probably come up with a better way to handle this.
my $bootlog;
if ($node->GetBootLog(\$bootlog) == 0) {
$args->{'bootlog'} = $bootlog;
}
if (defined($webtask)) {
# Cause of the fork in run_with_ssh.
$webtask->Refresh();
$webtask->status("finishing");
}
#
# If we timed out, if the result code was bad, or if the image size
# grew too large.
#
if ($result eq "setupfailed") {
$msg = "FAILED: Node setup failed ...";
goto out;
}
if ($result eq "timeout") {
$msg = "FAILED: Timed out generating image ...";
goto out;
}
if ($result eq "toobig") {
$msg = "FAILED: Maximum image size (".$self->maximagesize()." bytes) exceeded ...";
goto out;
}
if ($result ne "0") {
$msg = "FAILED: Returned error code $result generating image ...";
goto out;
}
# We want to Validate that the image made it to the repository;
# also, Validate has the side-effects of updating the DB with the
# size/hash fields.
($rc,$msg) = $self->Validate($image,{'update'=>1,'validate'=>{'all'=>1}});
if ($rc) {
$msg = "$self CaptureImage: failed validation: $msg";
goto out;
}
# Pick up the new size/hash info.
#$image->Refresh();
if (defined($webtask)) {
$webtask->Refresh();
my $sz = int($image->size());
$webtask->imagesize($sz / 1024)
if ($sz > 0);
}
my $cname = "$imagepid/$imagename";
$cname .= ":" . $image->version()
if ($args->{'doprovenance'});
print "$cname: ";
print "image creation succeeded, written to $path.\n";
"Final size: ".$image->size()." bytes.\n";
($rc,$msg) = $self->CreateImageFinalize($image,$node,$args);
goto out
if ($rc);
$rc = 0;
out:
$image->Unlock()
if ($needunlock);
if ($rc) {
tbwarn("$self CaptureImage: $msg\n");
}
if (wantarray) {
return ($rc,$msg);
}
else {
return $rc;
}
}
sub CLI_FORCE_NONE() { return 0; }
sub CLI_FORCE_TOKEN() { return 1; }
sub CLI_FORCE_SETUID() { return 2; }
sub GetDockerRegCLICommandPrefix($$$$$$$) {
my ($self,$op,$repo,$context,$force,$setuidref,$cleanupref) = @_;
if (!defined($force)) {
$force = CLI_FORCE_NONE();
}
my $dosetuid = 0;
my $this_user;
my $username;
if ($UID != 0) {
$this_user = User->ThisUser();
if (!defined($this_user)) {
return (undef,"You ($UID) do not exist!");
}
}
else {
$username = "root";
}
my $debugarg = "";
if ($self->debug()) {
$debugarg = "-d";
}
#
# First, see if we should be using the setuid root path, and the
# admin ssl cert.
#
if (($force == CLI_FORCE_NONE()
&& exists($ENV{"WITH_TB_ADMIN_PRIVS"})
&& $ENV{"WITH_TB_ADMIN_PRIVS"} eq "1")
|| $force == CLI_FORCE_SETUID()) {
# Check if we have setuid privs, so we can use the admin cert;
# or if we'll need a temp token. No other way I know to do this
# in perl other than to try!
if ($EUID != 0) {
my $SAVEEUID = $EUID;
$EUID = 0;
if ($EUID == 0) {
$dosetuid = 1;
}
$EUID = $SAVEEUID;
if ($dosetuid != 1) {
if ($force == CLI_FORCE_SETUID()) {
return (undef,"not setuid!");
}
else {
tbwarn("$self: GetDockerRegCLICommandPrefix: invoked with".
" wap but not setuid; falling back to token auth!\n");
$force = CLI_FORCE_TOKEN();
}
}
}
}
if ($dosetuid == 1 || ($force != CLI_FORCE_TOKEN()
&& ($UID == 0 || $EUID == 0))) {
$$setuidref = 1;
return ("$ADMINDOCKREGCLI $debugarg",undef);
}
else {
$$setuidref = 0;
}
if ($this_user->uid() eq 'geniuser') {
# If the current user is geniuser, there might be a localuser or
# not; so just use token auth for this.
$force = CLI_FORCE_TOKEN();
}
if ($force == CLI_FORCE_TOKEN()) {
my ($docker_token_user,$docker_token_pass);
$docker_token_user = $this_user->uid() . "-$context";
$docker_token_pass = TBGenSecretKey();
if (User->AddTokenPassword(
$this_user,$this_user,"docker.registry",$op,$repo,
$docker_token_user,$docker_token_pass,undef,600,600,0,1)) {
return (undef,
"Could not create temporary Docker Registry credentials");
}
else {
$$cleanupref = sub { User->DeleteTokenPasswords(
$this_user,$this_user,"docker.registry",
$docker_token_user,$docker_token_pass); };
}
return ("$DOCKREGCLI $debugarg".
" -u $docker_token_user -p $docker_token_pass",undef);
}
else {
my ($uid,$homedir) = ($this_user->uid(),$this_user->HomeDir());
return ("$DOCKREGCLI $debugarg".
" -u $uid -p '' --cert $homedir/.ssl/emulab.pem",undef);
}
}
sub DeleteImageFiles($$$) {
my ($self,$image,$args) = @_;
if (!defined($args)) {
$args = {};
}
my $versonly = $args->{'versonly'} || 0;
my $purge = $args->{'purge'} || 0;
my $rename = $args->{'rename'} || 0;
my $impotent = $args->{'impotent'} || 0;
my $msg = "";
my $rc = -1;
my $cmd;
#
# When doing image provenance, we have to deal with all versions
# of the image. This will not return deleted versions.
#
my @images = ();
if ($image->AllVersions(\@images)) {
$msg = "Could not get list of image (versions)";
goto out;
}
#
# When deleting just a single version, if this is the last or only
# version, then turn off version only. Makes no sense to have a
# descriptor with no non-deleted versions.
#
if ($versonly && scalar(@images) == 1) {
$versonly = 0;
}
if ($versonly) {
@images = ($image);
}
my $setuid = 0;
my $cleanup;
my $SAVEEUID;
#
# Grab our command prefix. If that creates a token for us, we use
# it for all versions.
#
foreach my $imageversion (@images) {
# Skip NULL or '' paths.
next
if (!defined($imageversion->path()) || $imageversion->path() eq "");
# Split the path into server, repo, and tag.
my ($server,$repo,$tag);
if ($imageversion->path() =~ /^([^\/]+)\/([^:]+):(.*)$/) {
($server,$repo,$tag) = ($1,$2,$3);
}
else {
$msg = "$self DeleteImageFiles: bad path ".$imageversion->path().
"; aborting!";
goto out;
}
if ($impotent) {
print STDERR "$self DeleteImageFiles: would have deleted" .
" $server $repo $tag\n";
next;
}
if ($rename) {
my $newtag = $imageversion->imageid() . "-$tag";
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.rename",$repo,"image-delete-rename",CLI_FORCE_NONE(),
\$setuid,\$cleanup);
if (!defined($cmd)) {
$msg = "$self DeleteImageFiles: $msg";
goto errout;
}
my $tcmd = "$cmd -s $server tag_image -r $repo -t $tag -n $newtag";
if ($self->debug()) {
print STDERR "$self DeleteImageFiles: running '$tcmd'\n";
}
if ($setuid) {
$SAVEEUID = $EUID;
$EUID = 0;
}
$rc = system($tcmd);
$EUID = $SAVEEUID
if ($setuid);
$cleanup->()
if (defined($cleanup));
if ($rc) {
$msg = "$self DeleteImageFiles: failed to rename tag" .
" $imageversion ($server $repo $tag) to $newtag!";
goto out;
}
$tcmd = "$cmd -s $server delete_tag -r $repo -t $tag";
if ($self->debug()) {
print STDERR "$self DeleteImageFiles: running '$tcmd'\n";
}
if ($setuid) {
$SAVEEUID = $EUID;
$EUID = 0;
}
$rc = system($tcmd);
$EUID = $SAVEEUID
if ($setuid);
$cleanup->()
if (defined($cleanup));
if ($rc) {
$msg = "$self DeleteImageFiles: failed to delete old tag" .
" in rename of $imageversion ($server $repo $tag) to" .
" $newtag!";
goto out;
}
}
elsif ($purge) {
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.pull",$repo,"image-delete-check",CLI_FORCE_NONE(),
\$setuid,\$cleanup);
if (!defined($cmd)) {
$msg = "$self DeleteImageFiles: $msg";
goto errout;
}
my $tcmd = "$cmd -s $server check_image -r $repo -t $tag";
if ($self->debug()) {
print STDERR "$self DeleteImageFiles: running '$tcmd'\n";
}
if ($setuid) {
$SAVEEUID = $EUID;
$EUID = 0;
}
$rc = system($tcmd);
$EUID = $SAVEEUID
if ($setuid);
$cleanup->()
if (defined($cleanup));
if ($rc) {
if ($self->debug()) {
print STDERR "$self DeleteImageFiles: $image ($server".
" $repo $tag) file already gone; nothing to delete.\n";
}
($rc,$msg) = (0,0);
goto out;
}
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.delete",$repo,"image-delete-tag",CLI_FORCE_NONE(),
\$setuid,\$cleanup);
if (!defined($cmd)) {
$msg = "$self DeleteImageFiles: $msg";
goto errout;
}
$tcmd = "$cmd -s $server delete_image -r $repo -t $tag";
if ($self->debug()) {
print STDERR "$self DeleteImageFiles: running '$tcmd'\n";
}
if ($setuid) {
$SAVEEUID = $EUID;
$EUID = 0;
}
$rc = system($tcmd);
$EUID = $SAVEEUID
if ($setuid);
$cleanup->()
if (defined($cleanup));
if ($rc) {
$msg = "$self DeleteImageFiles: failed to delete image" .
" $imageversion ($server $repo $tag)!";
goto out;
}
}
elsif ($self->debug()) {
print STDERR "$self DeleteImageFiles: nothing to do for" .
" $server $repo $tag\n";
}
}
$rc = 0;
out:
tbwarn("$self DeleteImageFiles: $msg\n")
if ($rc);
if (wantarray) {
return ($rc,$msg);
}
else {
return $rc;
}
}
sub Validate($$;$) {
my ($self,$image,$args) = @_;
my ($rc,$msg,$cmd,$userperm);
if (!defined($args)) {
$args = {};
}
if ($self->debug()) {
use Data::Dumper;
print STDERR "$self Validate: args: ".Dumper($args)."\n";
}
if (!defined($image) || !ref($image)
|| !($image->isa("Image") || $image->isa("OSImage"))) {
$msg = "image ($image) is not an Image!";
goto errout;
}
if ($args->{'update'}) {
$userperm = TB_IMAGEID_MODIFYINFO();
}
else {
$userperm = TB_IMAGEID_READINFO();
}
# Split the path into server, repo, and tag.
my ($server,$repo,$tag);
if ($image->path() =~ /^([^\/]+)\/([^:]+):(.*)$/) {
($server,$repo,$tag) = ($1,$2,$3);
}
else {
$msg = "Invalid Docker image path: ".$image->path()."!";
goto errout;
}
my $setuid = 0;
my $cleanup;
my $SAVEEUID;
#
# Ensure the manifest and layers are all present.
#
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.pull",$repo,"validate",CLI_FORCE_NONE(),\$setuid,\$cleanup);
if (!defined($cmd)) {
$msg = "$self Validate: $msg";
goto errout;
}
$cmd .= " -s $server check_image -r $repo -t $tag";
if ($self->debug()) {
print STDERR "$self: validating Docker image ($server,$repo,$tag)\n";
print STDERR "$self: validation command: $cmd\n";
}
if ($setuid) {
$SAVEEUID = $EUID;
$EUID = 0;
}
$rc = system($cmd);
$EUID = $SAVEEUID
if ($setuid);
$cleanup->()
if (defined($cleanup));
if ($rc) {
$msg = "Error validating Docker image!";
goto errout;
}
# Now grab the manifest metadata to populate the DB, if we're
# supposed to update:
if ($args->{'update'}) {
if ($self->debug()) {
print STDERR "$self: updating Docker image metadata" .
" ($server,$repo,$tag)\n";
}
my @lines;
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.pull",$repo,"validate",CLI_FORCE_NONE(),\$setuid,\$cleanup);
if (!defined($cmd)) {
$msg = "$self Validate: $msg";
goto errout;
}
$cmd .= " -s $server get_image_metadata -r $repo -t $tag";
if ($self->debug()) {
print STDERR "$self: metadata command: $cmd\n";
}
if ($setuid) {
$SAVEEUID = $EUID;
$EUID = 0;
}
@lines = `$cmd`;
$rc = $?;
$EUID = $SAVEEUID
if ($setuid);
$cleanup->()
if (defined($cleanup));
if ($rc) {
$msg = "Error getting Docker image reference metadata!";
goto errout;
}
if ($self->debug()) {
print STDERR "$self Validate: ".join('',@lines)."\n";
}
for my $line (@lines) {
chomp($line);
}
if ($args->{'validate'}{'all'} || $args->{'validate'}{'hash'}) {
if ($self->debug()) {
print STDERR "$self Validate: updating hashes\n";
}
my ($alg,$hash);
my ($lnum,$lalg,$lhash,$lsize);
my $maxlayer = -1;
for my $line (@lines) {
if ($line =~ /^digest:\s+([^:]+):([\w\d]+)$/) {
($alg,$hash) = ($1,$2);
if ($self->debug()) {
print STDERR "$self: digest: $alg, $hash\n";
}
}
elsif ($line =~ /^layer\[(\d+)\]:\s+([^:]+):(\w+),\s+(\d+)$/) {
if (int($1) > $maxlayer) {
$lnum = int($1);
$maxlayer = $lnum;
($lalg,$lhash,$lsize) = ($2,$3,$4);
}
if ($self->debug()) {
print STDERR "$self: layer: $1, $2, $3, $4\n";
}
}
}
if (!defined($alg) || !defined($hash)) {
$msg = "Unrecognized algorithm:hash digest for tag $tag!";
goto errout;
}
elsif ($alg ne 'sha256') {
$msg = "unexpected Docker digest algorithm $alg; not inserting!";
goto errout;
}
if (!defined($lalg) || !defined($lhash)) {
$msg = "Unrecognized algorithm:hash digest for layers for" .
" tag $tag!";
goto errout;
}
elsif ($lalg ne 'sha256') {
$msg = "unexpected Docker digest algorithm $alg for layer" .
" $lhash; not inserting as delta hash!";
goto errout;
}
$image->SetHash($hash);
$image->SetDeltaHash($lhash);
if ($self->debug()) {
print STDERR "$self Validate: set hash $hash, delta hash $lhash\n";
}
}
if ($args->{'validate'}{'all'} || $args->{'validate'}{'size'}) {
if ($self->debug()) {
print STDERR "$self Validate: updating sizes\n";
}
my ($size,$lalg,$lhash,$lsize,$lnum);
my $maxlayer = -1;
for my $line (@lines) {
if ($line =~ /^size:\s+(\d+)$/) {
$size = $1;
if ($self->debug()) {
print STDERR "$self: size: $size\n";
}
}
elsif ($line =~ /^layer\[(\d+)\]:\s+([^:]+):(\w+),\s+(\d+)$/) {
if (int($1) > $maxlayer) {
$lnum = int($1);
$maxlayer = $lnum;
($lalg,$lhash,$lsize) = ($2,$3,$4);
}
if ($self->debug()) {
print STDERR "$self: layer size: $1, $2, $3, $lsize\n";
}
}
}
if (!defined($size)) {
$msg = "No size for tag $tag!";
goto errout;
}
elsif (!defined($lsize)) {
$msg = "No layer size for tag $tag!";
goto errout;
}
$image->SetSize(int($size));
$image->SetDeltaSize(int($lsize));
if ($self->debug()) {
print STDERR "$self Validate: size $size, delta size $lsize\n";
}
}
#
# Sanitize the updated timestamp for any update. If it is NULL,
# that will break tmcd and anything else that assumes a non-null
# mtime. So, if it is NULL, one was never set; just use the
# ctime. Otherwise, do nothing.
#
my $stamp;
$image->GetUpdate(\$stamp);
if (!defined($stamp) || $stamp eq '' || $stamp == 0) {
$image->GetCreate(\$stamp);
$image->MarkUpdate(undef,$stamp);
}
}
out:
if (wantarray) {
return (0,"");
}
return 0;
errout:
tbwarn("$self Validate: $msg\n");
if (wantarray) {
return (-1,$msg);
}
return -1;
}
sub UpdateHash($$;$) {
my ($self,$image,$args) = @_;
my ($cmd,$msg,$rc);
my $debugarg = "";
if ($self->debug()) {
$debugarg = "-d";
}
my $userperm;
my $impotent = 0;
if (exists($args->{"impotent"})) {
$impotent = $args->{"impotent"};
}
if ($impotent) {
$userperm = TB_IMAGEID_MODIFYINFO();
}
else {
$userperm = TB_IMAGEID_READINFO();
}
if (!defined($image) || !ref($image)
|| !($image->isa("Image") || $image->isa("OSImage"))) {
$msg = "image ($image) is not an Image!";
goto errout;
}
# Split the path into server, repo, and tag.
my ($server,$repo,$tag);
if ($image->path() =~ /^([^\/]+)\/([^:]+):(.*)$/) {
($server,$repo,$tag) = ($1,$2,$3);
}
else {
$msg = "Invalid Docker image path: ".$image->path()."!";
goto errout;
}
if ($self->debug()) {
print STDERR "$self: updating Docker image hash ($server,$repo,$tag)\n";
}
my $setuid = 0;
my $cleanup;
my $SAVEEUID;
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.pull",$repo,"updatehash",CLI_FORCE_NONE(),\$setuid,\$cleanup);
if (!defined($cmd)) {
$msg = "$self UpdateHash: $msg";
goto errout;
}
$cmd .= " -s $server get_reference_metadata -r $repo --reference $tag";
if ($self->debug()) {
print STDERR "$self: validating Docker image ($server,$repo,$tag)\n";
print STDERR "$self: validation command: $cmd\n";
}
if ($setuid) {
$SAVEEUID = $EUID;
$EUID = 0;
}
my @lines = `$cmd`;
$rc = $?;
$EUID = $SAVEEUID
if ($setuid);
$cleanup->()
if (defined($cleanup));
if ($rc) {
$msg = "Error getting Docker image digest!";
goto errout;
}
my ($alg,$hash);
for my $line (@lines) {
chomp($line);
if ($line =~ /^digest:\s+([^:]+):(\w+)$/) {
($alg,$hash) = ($1,$2);
if ($self->debug()) {
print STDERR "$self: $alg, $hash\n";
}
last;
}
}
if (!defined($alg) || !defined($hash)) {
$msg = "Unrecognized algorithm:hash digest for reference $tag!";
goto errout;
}
elsif ($alg ne 'sha256') {
$msg = "unexpected Docker digest algorithm $alg; not inserting!";
goto errout;
}
if (!$impotent) {
if ($image->SetHash($hash) != 0) {
$msg = "Failed to set the hash for $image";
goto errout;
}
}
out:
if (wantarray) {
return ($hash,"");
}
return $hash;
errout:
tbwarn("$self UpdateHash: $msg\n");
if (wantarray) {
return (undef,$msg);
}
return undef;
}
sub Release($$;$) {
my ($self,$image,$args) = @_;
my ($rc,$msg) = (-1,"");
my ($quiet,$force,$markready,$impotent,$validate) = (0,0,0,0,1);
if (!defined($args)) {
$args = {};
}
if (exists($args->{'impotent'})) {
$impotent = $args->{'impotent'};
}
if (exists($args->{'quiet'})) {
$quiet = $args->{'quiet'};
}
if (exists($args->{'force'})) {
$force = $args->{'force'};
}
if (exists($args->{'markready'})) {
$markready = $args->{'markready'};
}
($rc,$msg) = $self->_ReleaseChecks($image,$args);
if ($rc) {
goto errout;
}
if ($force && $markready) {
if ($impotent) {
print "Would mark image ready/released, but not do anything else\n"
if (!$quiet);
}
else {
print "Marking image ready/released, but not doing anything else\n"
if (!$quiet);
$image->MarkReady();
$image->MarkReleased();
}
goto out;
}
my $needunlock = 0;
#
# Before we do anything destructive, we lock the image.
#
if ($image->Lock()) {
$msg = "Image is locked, please try again later!";
goto errout;
}
$needunlock = 1;
if ($validate) {
if ($impotent) {
print STDERR "Would run imagevalidate on " .
$image->versname() . "\n" if (!$quiet);
}
else {
if ($self->debug()) {
print STDERR "Running imagevalidate on " .
$image->versname() . "\n";
}
($rc,$msg) = $self->Validate($image);
if ($rc) {
$msg = "Failed to validate the image: $msg";
goto errout;
}
}
}
# Now mark as released.
if ($impotent) {
print STDERR "Would mark image as released\n"
if (!$quiet);
}
elsif ($image->Release()) {
$msg = "Could not mark image as released!";
goto errout;
}
out:
$image->Unlock()
if ($needunlock);
if (wantarray) {
return (0,"");
}
return 0;
errout:
$image->Unlock()
if ($needunlock);
tbwarn("$self Release: $msg\n");
if (wantarray) {
return (-1,$msg);
}
return -1;
}
sub ImportImageContent($$;$) {
my ($self,$image,$args) = @_;
if (!defined($args)) {
$args = {};
}
my ($rc,$msg) = (-1,"");
my ($origin_path,$newhash,$downloaded_ref,$force) = (undef,undef,undef,0);
my $locked = 0;
my $needunlock = 0;
if (exists($args->{'origin_path'})) {
$origin_path = $args->{'origin_path'};
}
else {
$msg = "No origin docker registry path provided";
goto errout;
}
if (exists($args->{'newhash'})) {
$newhash = $args->{'newhash'};
}
if (exists($args->{'downloaded_ref'})) {
$downloaded_ref = $args->{'downloaded_ref'};
}
if (exists($args->{'force'})) {
$force = $args->{'force'};
}
if (exists($args->{'locked'})) {
$locked = $args->{'locked'};
}
#
# Make sure we are already locked; lock if not.
#
if (!$locked) {
if ($image->Lock()) {
$msg = "$self ImportImageContent: $image is already locked,".
" please try again later ($PID)!";
goto out;
}
$needunlock = 1;
}
#
# Like in CaptureImage, we need to fixup the path to point to our
# local registry; and a few other things below:
#
my ($path,$server,$repo,$tag);
($path,$msg) = $self->GetLocalRegistryPathForImage(
$image,\$server,\$repo,\$tag);
if (!defined($path)) {
$rc = -1;
goto errout;
}
my $doit = 0;
if ((defined($newhash) && $newhash ne $image->hash()) || $force) {
$doit = 1;
}
else {
#
# Check to see if the image already exists at the path; don't
# need it if it's here.
#
my $setuid = 0;
my $cleanup;
my $SAVEEUID;
my $cmd;
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.pull",$repo,"validate",CLI_FORCE_NONE(),\$setuid,\$cleanup);
if (!defined($cmd)) {
tbwarn("$self ImportImageContent: $msg; skipping validation");
}
else {
$cmd .= " -s $server check_image -r $repo -t $tag";
if ($self->debug()) {
print STDERR "$self: checking Docker image ($server,$repo,$tag)\n";
print STDERR "$self: validation command: $cmd\n";
}
if ($setuid) {
$SAVEEUID = $EUID;
$EUID = 0;
}
$rc = system($cmd);
$EUID = $SAVEEUID
if ($setuid);
$cleanup->()
if (defined($cleanup));
if ($rc) {
if ($self->debug()) {
print STDERR "$self ImportImageContent: $image not".
" present; will download\n";
}
$doit = 1;
}
}
}
#
# Just return if we already have the right image and aren't forcing.
#
if (!$doit) {
goto out;
}
#
# Update the image to point to the repo/tag; change its format.
#
$image->SetPath($path);
$image->SetFormat("docker");
#
# Also update the image to run on type pcvm, so that we can do real
# osloads of this image on pcvms. This should probably be somewhere
# else, but no big deal.
#
$image->SetRunsOnNodeType('pcvm');
my ($rserver,$rrepo,$rtag);
if ($origin_path =~ /^([^\/]+)\/([^:]+):(.*)$/) {
($rserver,$rrepo,$rtag) = ($1,$2,$3);
}
else {
$msg = "$self ImportImageContent: could not extract remote docker".
" registry info from $origin_path!";
goto errout;
}
#
# Now we can pull and push the image all in one go, using our local
# admin credentials both times. NB: for this one, we *require*
# admin credential access so we can pull any image from the remote
# cluster.
#
my $setuid = 1;
my $cleanup;
my $SAVEEUID;
my $cmd;
my $idir = File::Temp->newdir(TEMPLATE=>"docker-image-XXXXXX",DIR=>"/tmp");
my $filebasename = $idir->dirname() . "image";
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.pull",$rrepo,"validate",CLI_FORCE_SETUID(),
\$setuid,\$cleanup);
if (!defined($cmd) || !$setuid) {
$msg = "$self ImportImageContent: error getting admin CLI prefix;".
" aborting!";
goto errout;
}
$cmd .= " -s $rserver pull_image -r $rrepo -t $rtag -f $filebasename";
if ($self->debug()) {
print STDERR "$self: pulling remote Docker image into $filebasename".
" ($rserver,$rrepo,$rtag)\n";
print STDERR "$self: command: $cmd\n";
}
$SAVEEUID = $EUID;
$EUID = 0;
$rc = system($cmd);
$EUID = $SAVEEUID;
if ($rc) {
$msg = "$self ImportImageContent: error pulling remote image!";
goto errout;
}
#
# And now we push it into our local registry.
#
($setuid,$cleanup) = (0,undef);
($cmd,$msg) = $self->GetDockerRegCLICommandPrefix(
"image.push",$repo,"push",CLI_FORCE_SETUID(),\$setuid,\$cleanup);
if (!defined($cmd) || !$setuid) {
$msg = "$self ImportImageContent: error getting admin CLI prefix;".
" aborting!";
goto errout;
}
$cmd .= " -s $server push_image -r $repo -t $tag -f $filebasename";
if ($self->debug()) {
print STDERR "$self: pushing remote Docker image from $filebasename".
" ($server,$repo,$tag)\n";
print STDERR "$self: command: $cmd\n";
}
$SAVEEUID = $EUID;
$EUID = 0;
$rc = system($cmd);
$EUID = $SAVEEUID;
if ($rc) {
$msg = "$self ImportImageContent: error pushing remote image to".
" local registry!";
goto errout;
}
#
# Now we validate the image, just as in CaptureImage.
#
if ($self->debug()) {
print STDERR "Running imagevalidate on " . $image->versname() . "\n";
}
($rc,$msg) = $self->Validate($image,{'update'=>'1','validate'=>{'all'=>1}});
if ($rc) {
$msg = "Failed to validate $image: $msg";
goto errout;
}
if (defined($downloaded_ref)) {
$$downloaded_ref = 1;
}
out:
$image->Unlock()
if ($needunlock);
if (wantarray) {
return (0,"");
}
return 0;
errout:
$image->Unlock()
if ($needunlock);
tbwarn("$self ImportImageContent: $msg\n");
if (wantarray) {
return (-1,$msg);
}
return -1;
}
1;