Index: trunk/Nebulous-Server/Build.PL
===================================================================
--- trunk/Nebulous-Server/Build.PL	(revision 39695)
+++ trunk/Nebulous-Server/Build.PL	(revision 39926)
@@ -48,4 +48,5 @@
         bin/neb-voladd
         bin/neb-voladm
+        bin/neb-aliasadd
         bin/neb-insedit
         bin/neb-host
Index: trunk/Nebulous-Server/bin/neb-aliasadd
===================================================================
--- trunk/Nebulous-Server/bin/neb-aliasadd	(revision 39926)
+++ trunk/Nebulous-Server/bin/neb-aliasadd	(revision 39926)
@@ -0,0 +1,231 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2016 Chris Waters
+#
+# $Id: neb-aliasadd
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.02';
+
+use DBI;
+use Nebulous::Server::SQL;
+use URI::file;
+use URI;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my ($db, $dbhost, $dbuser, $dbpass, $alias, $target, $update, $alias_id);
+
+$db     = $ENV{'NEB_DB'} unless $db;
+$dbhost = $ENV{'NEB_DBHOST'} || 'localhost';
+$dbuser = $ENV{'NEB_USER'} unless $dbuser;
+$dbpass = $ENV{'NEB_PASS'} unless $dbpass;
+
+GetOptions(
+    'db|d=s'            => \$db,
+    'host=s'            => \$dbhost,
+    'pass|p=s'          => \$dbpass,
+    'user|u=s'          => \$dbuser,
+    'alias_name=s'      => \$alias,
+    'target_name=s'     => \$target,
+    'alias_id=s'        => \$alias_id,
+    'update'            => \$update,
+) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --db --user --pass --alias_name --target_name", -exitval => 2 )
+    unless $db && $dbuser && $dbpass && $alias && $target;
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$db:host=$dbhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 1,
+    },
+);
+
+my $sql = Nebulous::Server::SQL->new();
+
+
+if ($update) {
+    unless ($alias && $target && $alias_id) {
+	pod2usage( -msg => "Required options for update: --db --user --pass --alias_name --target_name --alias_id", -exitval => 2 );
+    }
+    print "Updating alias...";
+    my ($vol_id, $tmp_name);
+    
+    my $vol_query = $dbh->prepare( $sql->get_volume_by_name );
+    $vol_query->execute( $target );
+    ($vol_id, $tmp_name, undef, undef) = $vol_query->fetchrow_array;
+    $vol_query->finish;
+    
+    unless ( (defined($vol_id))&& ($target eq $tmp_name)) {
+	die "ALIAS NOT CHANGED: ($target != $tmp_name) $vol_id not defined!";
+    }
+    
+    eval {
+	my $query = $dbh->prepare( $sql->update_alias );
+	# CHECK:
+#        UPDATE alias SET
+#          vol_id = ?,
+#          name   = ?
+#        WHERE alias_id = ?
+#        AND   alias    = ?
+	$query->execute( $vol_id, $target, $alias_id, $alias);
+    };
+    if ($@) {
+	die "database error: $@";
+    }
+    
+}
+else {
+    
+    print "Adding alias...";
+    my ($vol_id, $tmp_name);
+    
+    my $vol_query = $dbh->prepare( $sql->get_volume_by_name );
+    $vol_query->execute( $target );
+    ($vol_id, $tmp_name, undef, undef) = $vol_query->fetchrow_array;
+    $vol_query->finish;
+    
+    unless ( (defined($vol_id))&& ($target eq $tmp_name)) {
+	die "ALIAS NOT ADDED: ($target != $tmp_name) $vol_id not defined!";
+    }
+    
+    eval {
+	my $query = $dbh->prepare( $sql->new_alias );
+	$query->execute( $alias, $target, $vol_id);
+    };
+    if ($@) {
+	die "database error: $@";
+    }
+}
+
+print "OK $alias => $target\n";
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-aliasadd - Add a volume alias to the aliasvol table
+
+=head1 SYNOPSIS
+
+    neb-cabadd --target_name <target_volume> --alias_name <alias_volume>
+    [--db <database>] [--user <username>] [--pass <password>] [--host <hostname]
+
+=head1 DESCRIPTION
+
+This program creates a volume alias, allowing the database to correctly substitute
+the target volume when the alias volume is requested.  This can be used to ensure
+that Nebulous file reads choose the nearest local copy.
+
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --target_name <target_volume>
+
+Symbolic name of volume the alias should point to.
+
+=item * --alias_name <alias_volume>
+
+Symbolic name of volume the alias should point from.
+
+=item * --db|-d <database>
+
+Name of database (C<namespace>) to create tables in.
+
+Optional if the appropriate environment variable is set.
+
+=item * --user|-u <username>
+
+Username to authenticate with.
+
+Optional if the appropriate environment variable is set.
+
+=item * --pass|-p <password>
+
+Password to authenticate with.
+
+Optional if the appropriate environment variable is set.
+
+=item * --host <database host>
+
+Name of host on which the database resides.
+
+Optional.  Defaults to C<localhost>.
+
+=head1 ENVIRONMENT
+
+These environment variables may be used in place of the specified command line
+options.  All command line option will override the corresponding environment
+value.
+
+=over 4
+
+=item * C<NEB_DB>
+
+Equivalent to --db|-d
+
+=item * C<NEB_HOST>
+
+Equivalent to --host
+
+=item * C<NEB_USER>
+
+Equivalent to --user|-u
+
+=item * C<NEB_PASS>
+
+Equivalent to --pass|-p 
+
+=back
+
+=head1 CREDITS
+
+Josh Hoblitt and Chris Waters 
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+IPP
+
+=head1 COPYRIGHT
+
+Copyright (C) 2005-2008  Joshua Hoblitt.  All rights reserved.
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; either version 2 of the License, or (at your option) any later
+version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+Place - Suite 330, Boston, MA  02111-1307, USA.
+
+The full text of the license can be found in the L<perlgpl> Pod as supplied
+with Perl 5.8.1 and later.
+
+=head1 SEE ALSO
+
+L<Nebulous::Server>, L<neb-initdb>, L<neb-df>, L<nebdiskd>
+
+=cut
Index: trunk/Nebulous-Server/bin/neb-cabadd
===================================================================
--- trunk/Nebulous-Server/bin/neb-cabadd	(revision 39695)
+++ trunk/Nebulous-Server/bin/neb-cabadd	(revision 39926)
@@ -19,5 +19,5 @@
 use Pod::Usage qw( pod2usage );
 
-my ($db, $dbhost, $dbuser, $dbpass, $cname, $location);
+my ($db, $dbhost, $dbuser, $dbpass, $cname, $location, $site_id, $update, $cab_id);
 
 $db     = $ENV{'NEB_DB'} unless $db;
@@ -33,9 +33,12 @@
     'cname|n=s'         => \$cname,
     'location|l=s'      => \$location,
+    'site_id|s=s'       => \$site_id,
+    'cab_id=s'          => \$cab_id,
+    'update|u'          => \$update,
 ) || pod2usage( 2 );
 
 pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
-pod2usage( -msg => "Required options: --db --user --pass --cname", -exitval => 2 )
-    unless $db && $dbuser && $dbpass && $cname;
+pod2usage( -msg => "Required options: --db --user --pass --cname --site_id", -exitval => 2 )
+    unless $db && $dbuser && $dbpass && $cname && $site_id;
 
 my $dbh = DBI->connect(
@@ -52,11 +55,30 @@
 my $sql = Nebulous::Server::SQL->new();
 
-print "Adding cabinet...";
-
-my $query = $dbh->prepare( $sql->new_cabinet );
-$query->execute( $cname, $location);
-
-print " OK\n";
-
+if (defined($update)) {
+    unless ($cname && $location && $site_id && $cab_id) {
+	pod2usage( -msg => "Required options for update: --db --user --pass --cname --site_id --location --cab_id", -exitval => 2 );
+    }
+    print "Updating cabinet...";
+    my $query = $dbh->prepare( $sql->update_cabinet );
+    # Check query: 
+#        UPDATE cabinet SET
+#           location = ?,
+#           name     = ?,
+#           site_id  = ? 
+#        WHERE cab_id = ?
+
+    $query->execute( $location, $cname, $site_id, $cab_id );
+    
+    print "OK\n";
+
+}
+else {
+    print "Adding cabinet...";
+    
+    my $query = $dbh->prepare( $sql->new_cabinet );
+    $query->execute( $cname, $location, $site_id);
+    
+    print " OK\n";
+}
 __END__
 
@@ -69,5 +91,5 @@
 =head1 SYNOPSIS
 
-    neb-cabadd --cname <cabinet name> --location <cabinet location>
+    neb-cabadd --cname <cabinet name> --location <cabinet location> --site_id <site_id>
     [--db <database>] [--user <username>] [--pass <password>] [--host <hostname]
 
@@ -89,4 +111,8 @@
 
 Description of the location of the cabinet.
+
+=item * --site_id <site_id>
+
+Numerical value of the location's site.  Default = 0.
 
 =item * --db|-d <database>
Index: trunk/Nebulous-Server/lib/Nebulous/Server.pm
===================================================================
--- trunk/Nebulous-Server/lib/Nebulous/Server.pm	(revision 39695)
+++ trunk/Nebulous-Server/lib/Nebulous/Server.pm	(revision 39926)
@@ -1753,5 +1753,5 @@
 }
 
-sub find_instances
+sub find_instances_old
 {
     my $self = shift;
@@ -1820,4 +1820,157 @@
             # ext_id, name, available
             my $rows = $query->execute($key->path, $vol_name, 1);
+            unless ($rows > 0) {
+                $query->finish;
+                die("no instances on storage volume or volume is not avaiable for key: $key volume: $vol_name");
+            }
+        } else {
+            $query = $db->prepare_cached( $sql->get_object_instances );
+	    my $rows;
+            # ext_id, available
+	    if (defined($find_invalid)) {
+		$rows = $query->execute($key->path, 0);
+	    }
+	    else {
+		$rows = $query->execute($key->path, 1);
+	    }
+            unless ($rows > 0) {
+                $query->finish;
+                die("no instances available for key: $key");
+            }
+        }
+
+        while (my $row = $query->fetchrow_hashref) {
+            my $instance = $row->{ 'uri' };
+            push @locations, $instance if $instance;
+        }
+    };
+    if ($@) {
+        $db->rollback;
+        # handle soft volumes
+        if (defined $vol_name and not defined $key->hard_volume) {
+            $log->debug("retrying with 'any' volume");
+            return $self->find_instances($key->path, 'any');
+        }
+        $log->logdie("database error: $@");
+    }
+
+    # XXX remove this?
+    $log->logdie("no instances found") unless (scalar @locations);
+
+    $log->debug("found: @locations");
+
+    $log->debug("leaving");
+
+    return \@locations;
+}
+
+#sub find_instances_by_proximity
+sub find_instances
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key, $vol_name, $find_invalid) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR|UNDEF,
+#            callbacks   => {
+#                # check that the volume name requested is valid
+#                'is valid volume name' => sub {
+#                    return 1 if not defined $_[0];
+#                    $self->_is_valid_volume_name($_[0])
+#                },
+#            },
+            optional    => 1,
+        },
+	{
+            # find_invalid
+	    type        => SCALAR|UNDEF,
+            optional    => 1,
+        }, 
+    );
+
+    my $sql = $self->sql;
+
+#    unless ($key) {
+#        $log->warn("key was undefined after validate_pos(), trying again...");
+#        return $self->find_instances(@_);
+#    }
+
+    # vol_name overrides the key implied volume
+    my ($h_vol_id, $h_cab_id, $h_site_id);
+    eval {
+        $key = parse_neb_key($key, $vol_name);
+    };
+    $log->logdie("$@") if $@;
+    $vol_name = $key->volume;
+
+    my $db  = $self->db($key);
+
+    # Convert possible alias to real volume.
+    if (defined $vol_name) {
+	my ($tmp_vol_id, $tmp_name, $tmp_host, $tmp_path);
+	eval {
+	    my $query = $db->prepare_cached( $sql->get_volume_by_alias );
+	    $query->execute( $vol_name );
+	    ($tmp_vol_id, $tmp_name, $tmp_host, $tmp_path) = $query->fetchrow_array;
+	    $query->finish;
+	};
+	$log->logdie("$@") if $@;
+#	$log->warn("CZW: find_instance: deref alias: $vol_name => $tmp_vol_id $tmp_name $tmp_host $tmp_path (key vol: $key" . $key->volume . ")");
+	if (defined $tmp_vol_id and defined $tmp_name and defined $tmp_host and defined $tmp_path) {
+	    if ($tmp_name ne $vol_name) {
+		$vol_name = $tmp_name;
+#		$key->volume = $vol_name;
+	    }
+	}
+
+
+    }
+
+    # the key's volume can't be validiated on input for this method so we have
+    # to check it after parsing the key
+    if (defined $vol_name
+        and not $self->_is_valid_volume_name($key, $vol_name)) {
+        if ($key->hard_volume) {
+            $log->logdie("$vol_name is not a valid volume name");
+        } else {
+            $log->warn( "$vol_name is not a known volume name" );
+            $vol_name = undef;
+        }
+    }
+
+    # Get the host volume information encoded in the vol_name.
+    # I am unhappy that we have three different if(defined($vol_name)) entries, but I don't see a better way.
+#    my ($h_vol_id, $h_cab_id, $h_site_id);
+    if (defined $vol_name) {
+	eval {
+	    my $query = $db->prepare_cached( $sql->get_site_info_by_name );
+	    $query->execute( $vol_name );
+	    ($h_vol_id, $h_cab_id, $h_site_id) = $query->fetchrow_array;
+	    $query->finish;
+	};
+	$log->logdie("$@") if $@;
+
+	unless(defined $h_vol_id and defined $h_cab_id and defined $h_site_id) {
+	    $vol_name = undef;
+	}
+    }
+
+    my @locations;
+    eval {
+        my $query;
+        if ($vol_name && $h_vol_id && $h_cab_id && $h_site_id) {
+            $query = $db->prepare_cached( $sql->get_object_instances_by_proximity );
+            # ext_id, name, available
+	    # host_vol_id host_cab_id host_site_id ext_id available
+            my $rows = $query->execute($h_vol_id, $h_cab_id, $h_site_id, $key->path, 1);
             unless ($rows > 0) {
                 $query->finish;
@@ -2341,5 +2494,5 @@
     my $db  = $self->db($key);
 
-    my ($vol_id, $vol_host, $vol_path, $xattr, $forbidden_cabinet);
+    my ($vol_id, $vol_host, $vol_path, $xattr, $forbidden_cabinet, $forbidden_site);
     eval {
         my $rows;
@@ -2352,7 +2505,10 @@
 	}
 	if ($rows == 1) {
-	    ($forbidden_cabinet) = $query->fetchrow_array;
+	    ($forbidden_cabinet, $forbidden_site) = $query->fetchrow_array;
 	    unless (defined($forbidden_cabinet)) {
 		$forbidden_cabinet = 0;
+	    }
+	    unless (defined($forbidden_site)) {
+		$forbidden_site = 0;
 	    }
 	    $query->finish;
@@ -2360,4 +2516,5 @@
 	else {
 	    $forbidden_cabinet = 0;
+	    $forbidden_site    = 0;
 	    $query->finish;
 	}
@@ -2366,9 +2523,15 @@
         $query = $db->prepare_cached( $sql->get_replication_volume_for_ext_id );
         # ext_id, %free, avaiable, allocate
-        $rows = $query->execute($key->path, $max_used_space, 1, 1, $forbidden_cabinet, $topfew_count);
+        $rows = $query->execute($key->path, $max_used_space, 1, 1, $forbidden_cabinet, $forbidden_site, $topfew_count);
         # XXX destinguish between non-existant and unaviable
         unless ($rows > 0) {
             $query->finish;
-            die("can't find a suitable storage volume to replicate $key to");
+	    # CZW: 2016-08-23 This wasn't right.  If we don't get an entry, we may have been too strict.
+	    #      I'm not fully convinced this is complete, as we may want to back out the cabinet criterion as well.
+	    #      In any case, I don't think we're generally in the situation where replication can't find a host.
+	    $rows = $query->execute($key->path, $max_used_space, 1, 1, $forbidden_cabinet, 0, $topfew_count);
+	    unless ($rows > 0) {
+		die("can't find a suitable storage volume to replicate $key to");
+	    }
         }
         # when matching by name we shouldn't ever match more than once
@@ -2490,7 +2653,9 @@
     $log->logdie("database error: $@") if $@;
 
+#    $log->warn("CZW: $vol_id $vol_path for >>$vol_name<<");
     if (defined $vol_id and defined $vol_path) {
         $log->debug( "found volume name $vol_name" );
         $log->debug( "leaving" );
+#	$log->warn("CZW: $vol_id $vol_path for >>$vol_name<<");
         return 1;
     } 
Index: trunk/Nebulous-Server/lib/Nebulous/Server/SQL.pm
===================================================================
--- trunk/Nebulous-Server/lib/Nebulous/Server/SQL.pm	(revision 39695)
+++ trunk/Nebulous-Server/lib/Nebulous/Server/SQL.pm	(revision 39926)
@@ -250,4 +250,25 @@
             AND mountedvol.available = ?
     },
+    get_object_instances_by_proximity  => qq{
+        SELECT
+            storage_object.so_id,
+            uri,
+            mountedvol.available,
+            vol_id,
+            cab_id,
+            (ABS(vol_id - ?) + 20 * ABS(cab_id - ?) + 100 * ABS(site_id - ?)) AS vol_idx
+        FROM storage_object
+        JOIN instance
+            USING (so_id)
+        JOIN mountedvol
+            USING(vol_id)
+        JOIN volume
+            USING(vol_id)
+        JOIN cabinet
+            USING(cab_id)
+        WHERE ext_id = ?
+          AND mountedvol.available = ?
+        ORDER BY vol_idx ASC
+    },
     get_object_instances_by_vol_name => qq{
         SELECT
@@ -301,7 +322,8 @@
     get_cabinets_for_ext_id            => qq{
         SELECT DISTINCT 
-            cab_id 
+            volume.cab_id, site_id
         FROM instance 
-        JOIN volume ON instance.vol_id = volume.vol_id 
+        JOIN volume ON (instance.vol_id = volume.vol_id)
+        JOIN cabinet ON (volume.cab_id  = cabinet.cab_id)
         JOIN storage_object USING(so_id) 
         WHERE ext_id = ?
@@ -317,4 +339,5 @@
         FROM mountedvol AS m
         JOIN volume AS v USING(vol_id)
+        JOIN cabinet AS c USING(cab_id)
         LEFT JOIN (
                    SELECT
@@ -336,7 +359,9 @@
              AND m.available = ?
              AND m.allocate = ?
-             AND m.xattr = 0
+--             AND m.xattr = 0
              AND ( (v.cab_id IS NULL) ||
                    (v.cab_id != ?) )
+             AND ( (c.site_id IS NULL) ||
+                   (c.site_id != ?) )
          ORDER BY free DESC
          LIMIT ?) as topfew
@@ -366,10 +391,28 @@
     },
     new_cabinet         => qq{
-        INSERT INTO cabinet (name, location, cab_id)
-        VALUES (?, ?, NULL)
+        INSERT INTO cabinet (name, location, site_id, cab_id)
+        VALUES (?, ?, ?, NULL)
+    },
+    update_cabinet      => qq{
+        UPDATE cabinet SET 
+           location = ?,
+           name     = ?,
+           site_id  = ?
+        WHERE cab_id = ?
     },
     new_volume          => qq{
         INSERT INTO volume (name, host, path, allocate, available, xattr, mountpoint, cab_id, note)
         VALUES (?, ?, ?, TRUE, TRUE, FALSE, ?, NULL, ?)
+    },
+    new_alias          => qq{
+        INSERT INTO aliasvol (alias_id, alias, name, vol_id)
+        VALUES (NULL, ?, ?, ?)
+    },
+    update_alias       => qq{
+        UPDATE alias SET
+          vol_id = ?,
+          name   = ?
+        WHERE alias_id = ?
+        AND   alias    = ?
     },
     get_volume_by_name => qq{
@@ -377,4 +420,16 @@
         FROM volume
         WHERE name = ?
+    },
+    get_volume_by_alias => qq{
+        SELECT vol_id, name, host, path
+        FROM aliasvol
+        JOIN volume USING(vol_id,name)
+        WHERE alias = ?
+    },
+    get_site_info_by_name => qq{
+        SELECT vol_id, cab_id, site_id
+        FROM volume
+        JOIN cabinet USING(cab_id)
+        WHERE volume.name = ?
     },
     get_volumes => qq{
@@ -676,7 +731,9 @@
     cab_id INT NOT NULL AUTO_INCREMENT,
     name VARCHAR(255) NOT NULL,
+    site_id INT NOT NULL DEFAULT 0,
     location VARCHAR(255),
     PRIMARY KEY(cab_id),
     UNIQUE KEY(name),
+    KEY (site_id),
     KEY (location)
 ) ENGINE=innodb DEFAULT CHARSET=latin1;
@@ -735,4 +792,17 @@
 ###
 
+CREATE TABLE aliasvol (
+    alias_id INT NOT NULL AUTO_INCREMENT,
+    alias VARCHAR(255) NOT NULL,
+    name  VARCHAR(255) NOT NULL,
+    vol_id INT NOT NULL,
+    PRIMARY KEY(alias_id),
+    KEY(alias),
+    KEY(name),
+    FOREIGN KEY(vol_id) REFERENCES volume(vol_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
 CREATE TABLE instance (
     ins_id BIGINT NOT NULL AUTO_INCREMENT,
