Commit 66366489 authored by David Johnson's avatar David Johnson

Docker server-side core, esp new libimageops support for Docker images.

The docker VM server-side goo is mostly identical to Xen, with slightly
different handling for parent images.  We also support loading external
Docker images (i.e. those without a real imageid in our DB; in that
case, user has to set a specific stub image, and some extra per-vnode
metadata (a URI that points to a Docker registry/image repo/tag);
the Docker clientside handles the rest.

Emulab Docker images map to a Emulab imageid:version pretty seamlessly.
For instance, the Emulab `emulab-ops/docker-foo-bar:1` image would map
to `<local-registry-URI>/emulab-ops/emulab-ops/docker-foo-bar:1`; the
mapping is `<local-registry-URI>/pid/gid/imagename:version`.  Docker
repository names are lowercase-only, so we handle that for the user; but
I would prefer that users use lowercase Emulab imagenames for all Docker
images; that will help us.  That is not enforced in the code; it will
appear in the documentation, and we'll see.

Full Docker imaging relies on several other libraries
(https://gitlab.flux.utah.edu/emulab/pydockerauth,
https://gitlab.flux.utah.edu/emulab/docker-registry-py).  Each
Emulab-based cluster must currently run its own private registry to
support image loading/capture (note however that if capture is
unnecessary, users can use the external images path instead).  The
pydockerauth library is a JWT token server that runs out of boss's
Apache and implements authn/authz for the per-Emulab Docker registry
(probably running on ops, but could be anywhere) that stores images and
arbitrates upload/download access.  For instance, nodes in an experiment
securely pull images using their pid/eid eventkey; and the pydockerauth
emulab authz module knows what images the node is allowed to pull
(i.e. sched_reloads, the current image the node is running, etc).  Real
users can also pull images via user/pass, or bogus user/pass + Emulab
SSL cert.  GENI credential-based authn/z was way too much work, sadly.
There are other auth/z paths (i.e. for admins, temp tokens for secure
operations) as well.

As far as Docker image distribution in the federation, we use the same
model as for regular ndz images.  Remote images are pulled in to the
local cluster's Docker registry on-demand from their source cluster via
admin token auth (note that all clusters in the federation have
read-only access to the entire registries of any other cluster in the
federation, so they can pull images).  Emulab imageid handling is the
same as the existing ndz case.  For instance, image versions are lazily
imported, on-demand; local version numbers may not match the remote
image source cluster's version numbers.  This will potentially be a
bigger problem in the Docker universe; Docker users expect to be able to
reference any image version at any time anywhere.  But that is of course
handleable with some ex post facto synchronization flag day, at least
for the Docker images.

The big new thing supporting native Docker image usage is the guts of a
refactor of the utils/image* scripts into a new library, libimageops;
this is necessary to support Docker images, which are stored in their
own registry using their own custom protocols, so not amenable to our
file-based storage.  Note: the utils/image* scripts currently call out
to libimageops *only if* the image format is docker; all other images
continue on the old paths in utils/image*, which all still remain
intact, or minorly-changed to support libimageops.

libimageops->New is the factory-style mechanism to get a libimageops
that works for your image format or node type.  Once you have a
libimageops instance, you can invoke normal image logical operations
(CreateImage, ImageValidate, ImageRelease, et al).  I didn't do every
single operation (for instance, I haven't yet dealt with image_import
beyond essentially generalizing DownLoadImage by image format).
Finally, each libimageops is stateless; another design would have been
some statefulness for more complicated operations.   You will see that
CreateImage, for instance, is written in a helper-subclass style that
blurs some statefulness; however, it was the best match for the existing
body of code.  We can revisit that later if the current argument-passing
convention isn't loved.

There are a couple outstanding issues.  Part of the security model here
is that some utils/image* scripts are setuid, so direct libimageops
library calls are not possible from a non-setuid context for some
operations.  This is non-trivial to resolve, and might not be worthwhile
to resolve any time soon.  Also, some of the scripts write meaningful,
traditional content to stdout/stderr, and this creates a tension for
direct library calls that is not entirely resolved yet.  Not hard, just
only partly resolved.

Note that tbsetup/libimageops_ndz.pm.in is still incomplete; it needs
imagevalidate support.  Thus, I have not even featurized this yet; I
will get to that as I have cycles.
parent c5259a31
......@@ -811,7 +811,8 @@ sub ComputeNodeCounts($)
if (defined($virtualization_type) &&
($virtualization_type eq "emulab-xen" ||
$virtualization_type eq "emulab-blockstore")) {
$virtualization_type eq "emulab-blockstore" ||
$virtualization_type eq "emulab-docker")) {
$vcount++;
next;
}
......
......@@ -860,8 +860,9 @@ sub CheckFirewall($$)
# and closed.
#
my $style = "closed";
if (defined($virtualization_type) &&
$virtualization_type eq "emulab-xen" && !@routable_control_ip) {
if (defined($virtualization_type) && !@routable_control_ip &&
($virtualization_type eq "emulab-xen"
|| $virtualization_type eq "emulab-docker")) {
$style = "basic";
}
......
#!/usr/bin/perl -wT
#
# Copyright (c) 2007-2017 University of Utah and the Flux Group.
# Copyright (c) 2007-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -831,6 +831,8 @@ sub new($$$$$)
"jacks_site" => undef,
"xen_settings" => undef,
"xen_ptype" => undef,
"docker_settings" => undef,
"docker_ptype" => undef,
"instantiate_on" => undef,
"services" => [],
"statements" => [],
......@@ -1101,6 +1103,18 @@ sub addNode($$$)
$node->{"xen_settings"} = $settings;
last SWITCH;
};
# Docker settings
/^docker$/i && do {
my $settings = GeniXML::GetDockerSettings($ref->parentNode);
fatal("Failed to get docker settings")
if (!defined($settings));
$node->{"docker_settings"} = $settings;
last SWITCH;
};
/^docker_ptype$/i && do {
$node->{"docker_ptype"} = GetTextOrFail("name", $child);
last SWITCH;
};
/^(sliver_type_shaping|firewall_config)$/i && do {
# We handled this above.
last SWITCH;
......@@ -1291,7 +1305,7 @@ sub Compare($$)
last SWITCH;
};
(/^(component_id|component_manager_id|disk_image)$/i ||
/^(hardware_type|jacks_site|xen_ptype|instantiate_on)$/i ||
/^(hardware_type|jacks_site|xen_ptype|docker_ptype|instantiate_on)$/i ||
/^(adb_target|failure_action)$/i ||
/^(isexptfirewall|firewall_style)$/i ||
/^(use_type_default_image|routable_control_ip)$/i) && do {
......@@ -1304,7 +1318,7 @@ sub Compare($$)
# Handled up above in CompareNodes.
last SWITCH;
};
/^(xen_settings|desires|pipes|blockstores|attributes)$/i && do {
/^(xen_settings|docker_settings|desires|pipes|blockstores|attributes)$/i && do {
return 1
if (APT_Rspec::CompareHashes("Node: $client_id: $key",
$val1, $val2));
......
#!/usr/bin/perl -w
#
# Copyright (c) 2000-2017 University of Utah and the Flux Group.
# Copyright (c) 2000-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -201,6 +201,45 @@ sub GenerateNodeStatements($)
$node->addTagStatement("InstantiateOn('$vhost')");
}
}
elsif ($ntype eq "emulab-docker") {
$node->addStatement("$ntag = request.DockerContainer('$client_id')");
#
# This is the only time we need to spit this out, since
# the default is False.
#
if (defined($node->{'exclusive'}) && $node->{'exclusive'}) {
$node->addTagStatement("exclusive = True");
}
if (defined($node->{'docker_settings'})) {
my $settings = $node->{'docker_settings'};
foreach my $setting (sort(keys(%{$settings}))) {
my $value = $settings->{$setting};
if ($setting eq "ram") {
$node->addTagStatement("ram = $value");
}
elsif ($setting eq "cores") {
$node->addTagStatement("cores = $value");
}
elsif ($setting eq "extimage") {
$node->addTagStatement("docker_extimage = $value");
}
elsif ($setting eq "dockerfile") {
$node->addTagStatement("docker_dockerfile = $value");
}
}
}
if (defined($node->{'docker_ptype'})) {
my $ptype = $node->{'docker_ptype'};
$node->addTagStatement("docker_ptype = '$ptype'");
}
if (defined($node->{'instantiate_on'})) {
my $vhost = $node->{'instantiate_on'};
$node->addTagStatement("InstantiateOn('$vhost')");
}
}
elsif ($ntype eq "delay") {
#
# Bridges are also special, see comment above for blockstore.
......
......@@ -174,6 +174,7 @@ my %xmlfields =
"global", => ["global", $SLOT_ADMINONLY, 0],
"mbr_version", => ["mbr_version", $SLOT_OPTIONAL],
"makedefault", => ["makedefault", $SLOT_ADMINONLY, 0],
"format", => ["format", $SLOT_ADMINONLY, "ndz"],
);
#
......@@ -490,14 +491,27 @@ elsif (! $isadmin) {
UserError("Path: Invalid Path");
}
}
if ($newimageid_args{"path"} =~ /\/$/) {
if (-e $newimageid_args{"path"} && ! -d $newimageid_args{"path"}) {
UserError("Path: invalid path, it should be a directory");
}
if (defined($newimageid_args{"format"})
&& $newimageid_args{"format"} eq "docker") {
#
# We only allow a specific path for docker images, since the storage
# backend relies on an ACL of repos a user can access. It's not a
# filesystem with UNIX permissions. With a docker registry,
# permissions are tied to specific paths. Don't even let admins
# override this for now; there is no point.
#
}
elsif (-d $newimageid_args{"path"} =~ /\/$/) {
UserError("Path: invalid path, its a directory");
else {
if ($newimageid_args{"path"} =~ /\/$/) {
if (-e $newimageid_args{"path"} && ! -d $newimageid_args{"path"}) {
UserError("Path: invalid path, it should be a directory");
}
}
elsif (-d $newimageid_args{"path"} =~ /\/$/) {
UserError("Path: invalid path, its a directory");
}
}
#
# See what node types this image will work on. Must be at least one!
#
......
......@@ -210,6 +210,7 @@ my %xmlfields =
"global", => ["global", $SLOT_OPTIONAL, 0],
"noexport", => ["noexport", $SLOT_OPTIONAL, 0],
"mbr_version", => ["mbr_version", $SLOT_OPTIONAL],
"format", => ["format", $SLOT_OPTIONAL],
"metadata_url", => ["metadata_url", $SLOT_ADMINONLY],
"imagefile_url", => ["imagefile_url", $SLOT_ADMINONLY],
"origin_uuid", => ["origin_uuid", $SLOT_ADMINONLY],
......
#!/usr/bin/perl -wT
#
# Copyright (c) 2007-2017 University of Utah and the Flux Group.
# Copyright (c) 2007-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -638,7 +638,7 @@ sub Create($$$$$$$$)
}
# Pass-through optional slots, otherwise the DB default is used.
foreach my $key ("path", "shared", "global", "ezid", "mbr_version",
"metadata_url", "imagefile_url", "released",
"format", "metadata_url", "imagefile_url", "released",
"isdataset", "lba_size", "lba_low", "lba_high",
"origin_uuid", "origin_urn", "origin_name") {
if (exists($argref->{$key})) {
......@@ -1795,6 +1795,31 @@ sub GetDiskOffset($)
return $mbr{$self->mbr_version()}[$self->loadpart()];
}
#
# Return created time for image as a UNIX timestamp via the passed ref.
# Return 0 on success, non-zero otherwise.
#
sub GetCreate($$)
{
my ($self,$stampp) = @_;
# Must be a real reference.
return -1
if (! ref($self));
my $imageid = $self->imageid();
my $version = $self->version();
my $result =
DBQueryWarn("select UNIX_TIMESTAMP(created) from image_versions ".
"where imageid='$imageid' and version='$version'");
if ($result && $result->numrows) {
my ($stamp) = $result->fetchrow_array();
$$stampp = $stamp;
return 0;
}
}
#
# Return updated time for image as a UNIX timestamp via the passed ref.
# Return 0 on success, non-zero otherwise.
......@@ -2714,6 +2739,46 @@ sub SetImageMetadataURL($$)
return 0;
}
sub SetImageFileURL($$)
{
my ($self, $url) = @_;
$url = DBQuoteSpecial($url);
# Must be a real reference.
return -1
if (! ref($self));
my $imageid = $self->imageid();
my $version = $self->version();
DBQueryWarn("update image_versions set imagefile_url=$url ".
"where imageid='$imageid' and version='$version'")
or return -1;
$self->{'IMAGE'}->{'image_metadata_url'} = $url;
return 0;
}
sub SetFormat($$)
{
my ($self, $format) = @_;
return -1
if ($self->Update({'format' => $format}));
return 0;
}
sub SetPath($$)
{
my ($self, $path) = @_;
return -1
if ($self->Update({'path' => $path}));
return 0;
}
sub LocalURL($)
{
my ($self) = @_;
......@@ -2856,7 +2921,8 @@ sub IsDirPath($)
return 0;
}
return 1
if ($self->path() =~ /\/$/);
if ($self->path() =~ /\/$/
&& (!defined($self->format()) || $self->format() ne 'docker'));
return 0;
}
sub FullImagePath($)
......@@ -2958,7 +3024,9 @@ sub IsSystemImage($)
my ($self) = @_;
# Yuck.
return ($self->path() =~ /^\/usr\/testbed\// ? 1 : 0);
return ($self->path() =~ /^\/usr\/testbed\// ? 1 : 0
|| ($self->format() eq 'docker' && $self->global()
&& $self->path() =~ /emulab-ops\/emulab-ops/));
}
#
# When images are stored in sub directories.
......
#!/usr/bin/perl -wT
#
# Copyright (c) 2007-2017 University of Utah and the Flux Group.
# Copyright (c) 2007-2018 University of Utah and the Flux Group.
#
# {{{EMULAB-LICENSE
#
......@@ -662,6 +662,17 @@ sub Chunks($)
return $self->image()->Chunks();
}
#
# Return updated time for image as a UNIX timestamp via the passed ref.
# Return 0 on success, non-zero otherwise.
#
sub GetCreate($$)
{
my ($self,$stampp) = @_;
return $self->image()->GetCreate($stampp);
}
#
# Return updated time for image as a UNIX timestamp via the passed ref.
# Return 0 on success, non-zero otherwise.
......@@ -1052,6 +1063,20 @@ sub SetImageMetadataURL($$)
return $self->image()->SetImageMetadataURL($url);
}
sub SetFormat($$)
{
my ($self, $format) = @_;
return $self->image()->SetFormat($format);
}
sub SetPath($$)
{
my ($self, $path) = @_;
return $self->image()->SetPath($path);
}
sub LocalURL($)
{
my ($self) = @_;
......
......@@ -61,6 +61,7 @@ my %FEATURES = ( "ping" => 1,
"linktest" => 1,
"linkdelays" => 0,
"xen-host" => 0,
"docker-host"=> 0,
"suboses" => 0 );
# Valid OS names. Mirrored in the web interface. The value is a user-okay flag.
......
......@@ -788,6 +788,8 @@ sub Purge($)
or return -1;
DBQueryWarn("delete from user_policies where uid='$uid_idx'")
or return -1;
DBQueryWarn("delete from user_token_passwords where uid_idx='$uid_idx'")
or return -1;
return 0;
}
......@@ -2317,6 +2319,187 @@ sub UpdateExports($)
return 0;
}
#
# A simple interface to the user_token_passwords table. This table
# holds non-login passwords for user access to various testbed
# subsystems, each associated with a user account for accounting
# purposes. If you need a non-user oauth-like token, that will need
# another table. When users login with these usernames/passwords, they
# should be given an oauth (or similar) token.
#
# Table fields:
# * uid_idx - users.idx
# * uid - users.uid
# * subsystem - a name that identifies the subsystem/function/token issuer
# * scope_type - the type of access being granted
# * scope_value - the value of access being granted
# * username - the login username provided to the authentication endpoint;
# need not be the username!
# * plaintext - the plaintext value of the password; only necessary if the
# user should be able to retrieve it through some secure interface
# * hash - the password hash to be checked at authentication
# * expiration - the expiration time of this username/password; either an
# int (a unix timestamp) or a string (a local mysql datetime).
# * token_lifetime - the lifetime of each token generated for this
# username/password
# * token_onetime - 1 if each token generated for this username/password
# should be a one-time-use token; 0 otherwise
# * system - 1 if this is a system-created username necessary for an
# infrastructure service (i.e. capturing a disk image); 0 otherwise.
#
# Obviously, not all subsystems/token issuers would require or be
# capable of using all these fields; but this should be a good set for
# supporting a variety of OAuth tokens.
#
# There is a UNIQUE KEY set:
# UNIQUE KEY `user_token` (`subsystem`,`username`,`plaintext`)
# We want to allow a user to have multiple, same-named passwords per
# service; but they ought to have a different plaintext value. If we
# included hash in the key, we could not the latter restriction due to
# the random bits in the salt; thus we opt to only allow one empty
# password per username. This should be a fine compromise.
#
#
# Add a username/password, associated with an existing user account, to
# be used for token generation during authn/authz (i.e., OAuth):
#
# $user->AddTokenPassword($target_user,$subsystem,$scope_type,$scope_value,
# $username,$plaintext,$hash;$expiration,$token_lifetime,$token_onetime,
# $system)
#
# where subsystem must be set; scope_type and scope_value default to ''
# if undef; username defaults to this_user or target_user if unset. One
# of plaintext or hash must be set; if both are set,
# crypt(plaintext,hash) == hash must hold. plaintext defaults to '' if
# undef. expiration should be set by the caller if necessary for that
# subsystem; it defaults to NULL (never expires); token_lifetime, if
# unset, will be set to a default value by the subsystem token issuer;
# token_onetime defaults to 0 (i.e., token will be multi-user) if not
# set; system defaults to 0 if not set.
#
# This is a library function, never called based on direct input from
# the user; therefore we do not check the password strength! We are
# zealous in escaping input ags anyway.
#
sub AddTokenPassword($$$$$$$$$;$$$$)
{
my ($class, $this_user, $target_user, $subsystem, $scope_type, $scope_value,
$username, $plaintext, $hash,
$expiration, $token_lifetime, $token_onetime, $system) = @_;
my $this_uid = $this_user->uid();
my $isadmin = $this_user->IsAdmin();
my $target_uid = $target_user->uid();
my $target_uid_idx = $target_user->uid_idx();
my $status = $target_user->status();
return 1
if (!defined($subsystem) || !$subsystem);
return 1
if ((!defined($plaintext) || !$plaintext)
&& (!defined($hash) || !$hash));
return 1
if (defined($plaintext) && $plaintext ne ""
&& defined($hash) && $hash ne ""
&& crypt($plaintext,$hash) ne $hash);
if (!defined($username) || !$username) {
$username = $target_uid;
}
$scope_type = ""
if (!defined($scope_type));
$scope_value = ""
if (!defined($scope_value));
if (!defined($hash) || $hash eq "") {
#$rs = join("",(".","/",0..9,"A".."Z","a".."z")[map { rand(64) } (0..15)]);
#$hash = crypt($plaintext,"\$5\$$rs\$");
$hash = PassWordHash($plaintext);
return 2
if (!defined($hash) || $hash eq '');
}
my $qpt = DBQuoteSpecial($plaintext);
my $qpw = DBQuoteSpecial($hash);
my $exp = "NULL";
if (defined($expiration) && $expiration ne "") {
if ($expiration =~ /^\d+$/) {
$exp = "DATE_ADD(NOW(), INTERVAL $expiration SECOND)";
}
else {
$exp = DBQuoteSpecial($expiration);
}
}
my ($lt,$os,$sys) = (3600,0,0);
$lt = $token_lifetime
if (defined($token_lifetime) && $token_lifetime ne "");
$os = 1
if (defined($token_onetime) && $token_onetime);
$sys = 1
if (defined($system) && $system);
my $q = "insert into user_token_passwords".
" (uid_idx,uid,subsystem,scope_type,scope_value,username,plaintext,".
" hash,issued,expiration,token_lifetime,token_onetime) values".
" ('$target_uid_idx','$target_uid','$subsystem','$scope_type',".
" '$scope_value','$username',$qpt,$qpw,NOW(),$exp,$lt,$os)";
return 1
if (!DBQueryWarn($q));
return 0;
}
sub DeleteTokenPasswords($$$$$;$)
{
my ($class, $this_user, $target_user, $subsystem, $username,
$plaintext) = @_;
my $this_uid = $this_user->uid();
my $isadmin = $this_user->IsAdmin();
my $target_uid = $target_user->uid();
my $target_uid_idx = $target_user->uid_idx();
my $status = $target_user->status();
return 1
if (!defined($subsystem) || !$subsystem);
if (!defined($username) || !$username) {
$username = $target_uid;
}
my $q = "delete from user_token_passwords where".
" uid_idx='$target_uid_idx' and subsystem='$subsystem'".
" and username='$username'";
if (defined($plaintext)) {
$q .= " and plaintext=" . DBQuoteSpecial($plaintext);
}
return 1
if (!DBQueryWarn($q));
return 0;
}
sub DeleteTokenPasswordByIdx($$$$)
{
my ($class, $this_user, $target_user, $idx) = @_;
my $this_uid = $this_user->uid();
my $isadmin = $this_user->IsAdmin();
my $target_uid = $target_user->uid();
my $target_uid_idx = $target_user->uid_idx();
return 1
if (!defined($idx) || !$idx);
my $q = "delete from user_token_passwords where".
" uid_idx='$target_uid_idx' and idx=".DBQuoteSpecial($idx);
return 1
if (!DBQueryWarn($q));
return 0;
}
# _Always_ make sure that this 1 is at the end of the file...
1;
......@@ -1175,6 +1175,7 @@ sub GetTicketAuxAux($)
my $isbridge = 0;
my $isfirewall = 0;
my $xensettings;
my $dockersettings;
my $fwsettings;
# Always populate iface2node mapping, even if we let the node
......@@ -1410,6 +1411,50 @@ sub GetTicketAuxAux($)
$fwsettings = GeniXML::GetFirewallSettings($ref);
}
}
elsif ($virtualization_subtype eq "emulab-docker") {
# Allow caller to set the image to use, but also
# trick to set the parent.
if (defined($osinfo)) {
if (! $osinfo->IsSubOS()) {
$parent_osname = $osname;
$osname = "DOCKER-EXT";
}
}
elsif (!defined($osname)) {
# Allow for Docker image url via node attribute.
$osname = "DOCKER-EXT";
}
# If not set, we will pick up the default_image below.
#
# Look for the knobs
#
if (GeniXML::HasDockerSettings($ref)) {
$dockersettings = GeniXML::GetDockerSettings($ref);
}
# If they want an external image (or we
# defaulted to one), but one isn't set, set a
# good default.
if ($osname eq 'DOCKER-EXT') {
if (!defined($dockersettings)) {
$dockersettings = {};
}
if (!exists($dockersettings->{'extimage'})
&& !exists($dockersettings->{'dockerfile'})) {
$dockersettings->{'extimage'} = "ubuntu:16.04";
}
}
my $ptype = GeniXML::DockerPtype($ref);
$pctype = $ptype
if (defined($ptype));
$virtexperiment->encap_style("vlan");
#
# Per-vnode firewall options.
#
if (GeniXML::HasFirewallSettings($ref)) {
$fwsettings = GeniXML::GetFirewallSettings($ref);
}
}
elsif ($virtualization_subtype eq "emulab-spp") {
$osname = "SPPVM-FAKE";
$pctype = "sppvm";
......@@ -1632,7 +1677,8 @@ sub GetTicketAuxAux($)
#
if (defined($instantiate_on)) {
if (defined($virtualization_subtype) &&
$virtualization_subtype eq "emulab-xen") {
($virtualization_subtype eq "emulab-xen" ||
$virtualization_subtype eq "emulab-docker")) {
if (!exists($allnodes{$instantiate_on})) {
$response =
......@@ -1644,7 +1690,7 @@ sub GetTicketAuxAux($)
else {
$response =
GeniResponse->Create(GENIRESPONSE_BADARGS, undef,
"Only Xen nodes can use 'instantiate_on'");
"Only Xen or Docker nodes can use 'instantiate_on'");
goto bad;
}
}
......@@ -2090,6 +2136,111 @@ sub GetTicketAuxAux($)
"attrvalue" => $attrvalue });
}
}
if (defined($dockersettings)) {
foreach my $setting (keys(%$dockersettings)) {
my $attrvalue = $dockersettings->{$setting};
my $attrkey;
my $isshared = (!defined($exclusive) || !$exclusive ? 1 : 0);
if ($setting eq "ram") {
if ($attrvalue !~ /^\d*$/) {
$response = GeniResponse->Create(
GENIRESPONSE_BADARGS, undef,
"Bad Docker ram value; integers only");
goto bad;
}
if ($isshared && 0 && $attrvalue > 1024) {
$response = GeniResponse->Create(
GENIRESPONSE_BADARGS, undef,
"Bad Docker shared node ram value;".
" limited to 1024 MB");
goto bad;
}
$attrkey = "DOCKER_MEMSIZE";
}
elsif ($setting eq "cores") {
if ($attrvalue !~ /^\d*$/) {
$response = GeniResponse->Create(
GENIRESPONSE_BADARGS, undef,
"Bad Docker cores value; integers only");
goto bad;
}
if ($attrvalue <= 0) {
# ignore silly user.
next;
}
$attrkey = "DOCKER_CORES";
if ($isshared && $attrvalue > 4) {
$response = GeniResponse->Create(
GENIRESPONSE_BADARGS, undef,
"Bad Docker shared node cores value; ".
"limited to 1-4 cores");
goto bad;
}
}
elsif ($setting eq "extimage") {
$attrkey = "DOCKER_EXTIMAGE";
}
elsif ($setting eq "extusername") {
$attrkey = "DOCKER_EXTUSER";
}
elsif ($setting eq "extpassword") {
$attrkey = "DOCKER_EXTPASS";
}
elsif ($setting eq "dockerfile") {
$attrkey = "DOCKER_DOCKERFILE";
}
elsif ($setting eq "tbaugmentation") {
if ($attrvalue ne 'full' && $attrvalue ne 'buildenv'
&& $attrvalue ne 'core' && $attrvalue ne 'basic'
&& $attrvalue ne 'none') {
$response = GeniResponse->Create(
GENIRESPONSE_BADARGS, undef,
"Bad Docker tbaugmentation value; must be full,".
" buildenv, core, basic, or none");
goto bad;
}
$attrkey = "DOCKER_EMULABIZATION";
}
elsif ($setting eq "tbaugmentation_update") {
$attrkey = "DOCKER_EMULABIZATION_UPDATE";
}
elsif ($setting eq "ssh_style") {
if ($attrvalue ne 'direct' && $attrvalue ne 'exec') {
$response = GeniResponse->Create(
GENIRESPONSE_BADARGS, undef,
"Bad Docker ssh_style value; must be direct or exec");
goto bad;
}
$attrkey = "DOCKER_SSH_STYLE";
}
elsif ($setting eq "exec_shell") {
if ($attrvalue !~ /^[\/\w\d_\-]+$/) {
$response = GeniResponse->Create(
GENIRESPONSE_BADARGS, undef,
"Bad Docker exec_shell value; can only have".
" alphanumeric chars, _, -, or /");
goto bad;
}
$attrkey = "DOCKER_EXEC_SHELL";
}
elsif ($setting eq "cmd") {
$attrkey = "DOCKER_CMD";
#$attrvalue = DBQuoteSpecial($attrvalue);
}
elsif ($setting eq "env") {