Index: /branches/eam_branches/neb-host-20250516/Build.PL
===================================================================
--- /branches/eam_branches/neb-host-20250516/Build.PL	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/Build.PL	(revision 42864)
@@ -0,0 +1,63 @@
+use Module::Build;
+# See perldoc Module::Build for details of how this works
+
+Module::Build->new(
+    module_name         => 'Nebulous',
+    dist_version_from   => 'lib/Nebulous/Server.pm',
+    author              => 'Joshua Hoblitt <jhoblitt@cpan.org>',
+    license             => 'gpl',
+    create_makefile_pl  => 'passthrough',
+    requires            => {
+        'Apache2::Const'        => 0,
+        'Cache::Memcached'      => 0,
+        'Class::Accessor::Fast' => 0,
+        'Config::YAML'          => '1.42',
+        'DBD::mysql'            => '3.0007',
+        'DBI'                   => '1.53',
+        'File::Basename'        => 0,
+        'File::ExtAttr'         => '1.03',
+# needed for Ubuntu, not Gentoo
+        'Digest::SHA'           => 0,
+# not needed for Ubuntu
+#       'Digest::SHA1'          => 0,
+#       'File::Mountpoint'      => '0.01',
+#       'Filesys::Df'           => '0.92',
+        'File::Path'            => '1.08',
+        'File::Spec'            => 0,
+        'File::Spec::Functions' => 0,
+        'File::Temp'            => 0,
+        'Log::Dispatch::Email::MailSend' => 0,
+        'Log::Log4perl'         => '0.48',
+        'Net::Server::Daemonize'=> '0.05',
+        'Params::Validate'      => '0.73',
+        'SOAP::Lite'            => '0.69',
+        'SQL::Interp'           => '1.06',
+        'URI'                   => '1.30',
+    },
+    build_requires      => {
+        'Test::More'            => '0.49',
+#        'Test::URI'             => '1.06',
+#        'Test::DBUnit'          => '0.20',
+    },
+    recommends          => {
+        'Test::Distribution'    => '1.22',
+# needed for Gentoo, not Ubuntu
+#       'Apache2::SOAP'         => 0,
+# needed for Ubuntu, not Gentoo
+        'Apache::SOAP'         => 0,
+        'Apache::DBI'           => '1.05',
+    },
+    script_files        => [qw(
+        bin/neb-cabadd
+        bin/neb-voladd
+        bin/neb-voladm
+        bin/neb-aliasadd
+        bin/neb-insedit
+        bin/neb-host
+        bin/nebdiskd
+    )],
+)->create_build_script;
+
+# these scripts are no longer installed (2019.10.31)
+#       bin/neb-admin
+#       bin/neb-fsck
Index: /branches/eam_branches/neb-host-20250516/Changes
===================================================================
--- /branches/eam_branches/neb-host-20250516/Changes	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/Changes	(revision 42864)
@@ -0,0 +1,233 @@
+Revision history for Nebulous
+
+0.17
+    - add basic valid key caching via memcached 
+    - add params checking to Nebulous::Key::parse_neb_key()
+    - restrict creation/modification/remove of xattrs to the user.* namespace
+    - add chmod_object() method
+    - retry database transactions when a deadlock is detected
+    - add log4perl logging to nebdiskd
+    - base the on disk directory hashing of files only on the dirname()
+      component of keys
+    - add email logging of events to nebdiskd
+    - attempt to optimize _is_valid_object_key() by eliminating an unused join
+    - rename Nebulous::Keys class -> Nebulous::Key
+    - select neb db to use by hashing only the directory component of the key
+    - disallow Nebulous::Server->rename_object() when it would cause the db
+      hash of a key to change
+    - create a pseduo directory structure on key creation
+    - Nebulous::Key parsing and testing improvements
+    - change 'log_level' param to 'trace'
+    - refactor ->find_objects() functionality
+    - rename Nebulous::Key->soft_volume Nebulous::Key->hard_volume and invert
+      the semantics
+    - add nebdiskd mountpoint test retyring
+    - rework delete_instance() to avoid requiring an index on instance.uri
+    - nebdiskd bug fixes: fix debug mode outpoint going to mail, remove db
+      passwd requirement (not all dbs require a password param)
+    - completely rework how mountedvol is populated, drop mount table
+    - infinitely try to get a db handle if the connection fails
+    - add --mountpoint param to neb-voladd
+    - change Nebulous::Server->find_objects() to return dirs and to sort it's
+      output 
+    - add the ability to delete a storage object when it has instances that are
+      offline
+    - change Nebulous::Server->stat_object() to return both the total number of
+      instance and just those that are available
+    - add Nebulous::Server->prune_object() API
+    - don't replicate to the volume being implied by the key being replicated!
+      
+0.16
+    - add so_id/name idxs to storage_object_xattr table
+    - rename neb-addvol -> neb-voladd
+    - add a pid file to neb-admin so only one instance can be run at a time
+    - rework nebdiskd to only record data on volumes it is supposed to be
+      monitoring and then only for paths that actually have volumes mounted on
+      them
+    - fix getmountedvol() stored proc to not add volumes to the mountedvol
+      table that are not available
+    - dump Sys::Statistics::Linux::DiskUsage for Filesys::Df in nebdiskd as
+      it's behind the scenes usage of `df` instead of statfs() was causing lags
+      on hung NFS mounts
+    - document neb-voladd --host option
+    - add --user and --group options to nebdiskd to select the user/group that
+      daemon will run as
+    - fix nebulous --stop to return a status of 0 on success
+    - add gentoo init script
+    - remove call to getmountedvol() stored proc from Nebulous::Server->mount()
+      as it's uneeded
+    - change getmountedvol() to use a view of the volume table to avoid table
+      level locks being set by a cursor
+    - drop instance.sha1sum and instance.assigned_vol_id
+    - change find_objects_with_unavailable_instances query to work on a
+      temporary table instead of directly on the mountedvol table
+    - change neb-admin to properly cleanup it's semaphore file
+    - retry system call failures
+    - change Nebulous::Server->new() to call ->db() instead of setting up the
+      DBH itself; cleanup Nebulous::Server->db()
+    - add Nebulous::Server->swap_objects() method
+    - change nebdiskd to not do things that might imply a table level lock on
+      the mount table
+    - add neb-voladm util
+
+0.15 Thu Sep 11 13:00:59 HST 2008
+    - invert soft volume semantics
+
+0.14 Wed Aug 20 16:25:43 HST 2008
+    - change the get_replication_volume_for_ext_id query to choose a storage
+      volume (that meets the critera) at random
+
+0.13 Wed Jul  9 16:36:27 HST 2008
+    - create a Nebulous::Keys object to represent a parsed neb key and
+      impliment other changes to abstract out the handling of keys in order to
+      support "soft" volume requests and the "any" volume
+    - change find_objects_with_unavailable_instances query to only count
+      volumes marked as 'available'
+    - fix nebdiskd so it doesn't removed volumes as being successful stat()'d
+      even when the stat() failed under --debug
+    - add "smart" storage volume selection when ->replicate_object() is invoked without a target volume name
+    - add volume.host field
+    - add --host option to neb-initdb
+    - add --host option to neb-addvol
+    - add --host option to neb-admin
+    - add --host option to nebdiskd
+    - decalare default SQL character set
+
+0.12 Wed May 28 11:03:06 HST 2008
+    - remove the use of LAST_INSERT_ID() from the new_object_instance query
+    - add neb-admin util
+    - fix Nebulous::Server::setxattr_object() so that creating a new xattr with
+      the "replace" flag works
+
+0.11 Tue May 20 10:54:50 HST 2008
+    - add support for the magical ':any' volume to
+      Nebulous::Server:find_instances()
+    - call sync on newly created files from
+      Nebulous::Server::_create_empty_file()
+    - improved error strings in Nebulous::Server::find_instances()
+
+0.10 Wed May 14 17:26:48 HST 2008
+    - cleanup error handling in Nebulous::Server::stat_object()/find_objects()
+    - update Nebulous::Server::lock_object()/unlock_object() fault strings
+    - cleanup database error handling: make sure queries are ->finished before
+      showing an expect, remove DBI->rollback from the error paths of methods
+      that don't insert data
+    - improve getxattr_object() error handling
+    - document xattr methods
+    - change Nebulous::Server::stat() to not die when passed a non-existant key
+    - change listxattr_object() to return an arrayref
+    - change default log level to 'WARN'
+    - fix _is_valid_object_key() to handle nebulous keys
+
+0.09 Tue May  6 12:12:02 HST 2008
+    - allow volume name arguments to be undef
+
+0.08 Thu Apr 17 16:08:16 HST 2008
+    - set permissions on storage instance directories
+
+0.07
+    - add Nebulous::Server::mounts()
+    - server/client split into Nebulous::Server and Nebulous::Client packages
+    - add support for storing instances in multi-tier 'hashed' directories 
+    - pickup test DB settings from the env.
+    - add neb-ls -l|-1 option
+    - add neb-fsck
+    - add neb-stat
+    - add additional fkey constrains to the instance table
+    - fix installation of neb-replicate & neb-cull
+    - redo database schema with foreign keys
+    - add Nebulous::Keys pod
+
+0.06 Fri Jan 25 10:55:59 HST 2008
+    - define Nebulous::Client::find_objects() as returning [] when NO keys are
+      found
+    - change neb-touch to use Nebulous::Client::find_objects() instead of
+      ::find()
+    - add Nebulous::Sever:: neb key/uri support
+    - add Nebulous::Util::parse_neb_key()
+    - change bin/* scripts to conistently use --volume instead of --node,
+      etc.
+    - pod cleanups
+    - add neb-replicate & neb-cull
+
+0.05 2007-05-04 16:59:31(???)
+    - make filesystem xattr support optional & disabled by default
+    - generate instance filenames that leave the "key" at the end so as to not
+      break a pplications that expect a certain "extension" on the filename
+    - set an xattr on all created files of "user.nebulous_key"
+    - add neb-locate
+    - change all Nebulous::Client public methods to behave in list context
+    - rework the getmountedvol() stored procedure to attempt to work around
+      what appears to be some nasty transacational isolation leak throught that
+      was causing getmountedvol() to randomly hang when nebdiskd changed the
+      mount table.
+    - fix a nasty logic bug in Nebulous::Client->replicate()
+    - break 1:1 relationship between key names and on disk file names
+    - overhaul Nebulous::Client->cull()
+    - add the number of instances to ->stat_object()
+    - SQL cleanup
+    - better paramter checking
+    - overhaul instance table
+    - add ->_is_valid_object_key() method
+    - change ->find_instances() to check it's params and only return instances
+      on mounted & available volumes
+    - add volume.available field and logic to use it
+    - add volume.allocate field and logic to use it
+    - add instance.vol_id field
+    - remove $class_id & $comment params from Nebulous::Server->create() and
+      Nebulous::Client->create*() and all supporting functions and tests
+    - add Nebulous::Server->{setxattr_object, listxattr_object,
+      getxattr_object, removexattr_object}()
+    - add neb-cat
+    - add neb-mv
+    - change Nebulous::Client->move() to use ->rename_object()
+    - add Nebulous::Server->rename_object()
+    - add neb-rm
+    - add neb-cp
+    - add neb-ls
+    - add Nebulous::Client->find_objects()
+    - add Nebulous::Server->find_objects()
+    - add neb-touch
+    - add neb-df
+    - change neb-addvol to properly mangle URIs into paths
+
+0.04 Mon Apr 23 13:33:16 HST 2007
+    - include autogen.sh in the tarball
+    - add nebdiskd
+    - [nebclient] VERSION to '0.0.3'
+    - change API to generally return true URIs instead of file paths
+    - add support for some what intelligent volume allocations
+    - minor code & build fixes, ws changes
+
+0.03 Mon Dec 11 14:19:45 HST 2006
+    - change Nebulous::Server instantiate objects (init -> new) instead of
+      working with class data
+    - add a DESTROY method to Nebulous::Server to tear down the database
+      connection
+    - add a Nebulous::Server->config() accessor
+    - make Nebulous::Server->db() Apache::DBI aware
+    - make SQL syntax MySQL 5 compatible
+    - update SOAP, DBI & DBD, and Apache::* deps
+    - change Nebulous::Server::SOAP to be mod_perl aware
+    - change Nebulous::Server::SOAP install stub methods to bypass AUTOLOAD on
+      successive calls to the same method
+    - make Nebulous::Server::Log mod_perl aware
+    - change Nebulous::Server::Log to accept a Nebulous::Server::Config object
+      as a parameter instead of fetching it as a singleton
+    - make Nebulous::Server::Config no longer a singleton
+
+0.02 Fri Dec  2 17:36:59 HST 2005
+    - recommend Test::Distribution 1.22
+    - add t/00_distribution.t
+    - rename test files
+    - [nebclient] VERSION to '0.0.2'
+    - Nebulous::Client, Nebulous::Server, & Nebulous::Server::SQL
+        VERSION to '0.02'
+    - add Nebulous::Client->open_create()
+    - change Nebulous::Client->create() to return just a filename
+    - [nebclient] add nebOpenCreate()
+    - [nebclient] change nebCreate() to return just a filename
+
+0.01 Fri Sep  3 10:23:31 2004
+    - first working version
+	- original version; created by ExtUtils::ModuleMaker 0.32
Index: /branches/eam_branches/neb-host-20250516/LICENSE
===================================================================
--- /branches/eam_branches/neb-host-20250516/LICENSE	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/LICENSE	(revision 42864)
@@ -0,0 +1,260 @@
+The General Public License (GPL)
+Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave,
+Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute
+verbatim copies of this license document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share
+and change it. By contrast, the GNU General Public License is intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users. This General Public License applies to most of
+the Free Software Foundation's software and to any other program whose
+authors commit to using it. (Some other Free Software Foundation software is
+covered by the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for this service if you wish), that
+you receive source code or can get it if you want it, that you can change the
+software or use pieces of it in new free programs; and that you know you can do
+these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny
+you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a
+fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show
+them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer
+you this license which gives you legal permission to copy, distribute and/or
+modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients to
+know that what they have is not the original, so that any problems introduced by
+others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish
+to avoid the danger that redistributors of a free program will individually obtain
+patent licenses, in effect making the program proprietary. To prevent this, we
+have made it clear that any patent must be licensed for everyone's free use or
+not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+GNU GENERAL PUBLIC LICENSE
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND
+MODIFICATION
+
+0. This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms of
+this General Public License. The "Program", below, refers to any such program
+or work, and a "work based on the Program" means either the Program or any
+derivative work under copyright law: that is to say, a work containing the
+Program or a portion of it, either verbatim or with modifications and/or translated
+into another language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by
+this License; they are outside its scope. The act of running the Program is not
+restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made by
+running the Program). Whether that is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source code as
+you receive it, in any medium, provided that you conspicuously and appropriately
+publish on each copy an appropriate copyright notice and disclaimer of warranty;
+keep intact all the notices that refer to this License and to the absence of any
+warranty; and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at
+your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it, thus
+forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you also
+meet all of these conditions:
+
+a) You must cause the modified files to carry prominent notices stating that you
+changed the files and the date of any change.
+
+b) You must cause any work that you distribute or publish, that in whole or in
+part contains or is derived from the Program or any part thereof, to be licensed
+as a whole at no charge to all third parties under the terms of this License.
+
+c) If the modified program normally reads commands interactively when run, you
+must cause it, when started running for such interactive use in the most ordinary
+way, to print or display an announcement including an appropriate copyright
+notice and a notice that there is no warranty (or else, saying that you provide a
+warranty) and that users may redistribute the program under these conditions,
+and telling the user how to view a copy of this License. (Exception: if the
+Program itself is interactive but does not normally print such an announcement,
+your work based on the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be reasonably
+considered independent and separate works in themselves, then this License,
+and its terms, do not apply to those sections when you distribute them as
+separate works. But when you distribute the same sections as part of a whole
+which is a work based on the Program, the distribution of the whole must be on
+the terms of this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your rights to
+work written entirely by you; rather, the intent is to exercise the right to control
+the distribution of derivative or collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program with the
+Program (or with a work based on the Program) on a volume of a storage or
+distribution medium does not bring the other work under the scope of this
+License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1 and 2
+above provided that you also do one of the following:
+
+a) Accompany it with the complete corresponding machine-readable source
+code, which must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange; or,
+
+b) Accompany it with a written offer, valid for at least three years, to give any
+third party, for a charge no more than your cost of physically performing source
+distribution, a complete machine-readable copy of the corresponding source
+code, to be distributed under the terms of Sections 1 and 2 above on a medium
+customarily used for software interchange; or,
+
+c) Accompany it with the information you received as to the offer to distribute
+corresponding source code. (This alternative is allowed only for noncommercial
+distribution and only if you received the program in object code or executable
+form with such an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for making
+modifications to it. For an executable work, complete source code means all the
+source code for all modules it contains, plus any associated interface definition
+files, plus the scripts used to control compilation and installation of the
+executable. However, as a special exception, the source code distributed need
+not include anything that is normally distributed (in either source or binary form)
+with the major components (compiler, kernel, and so on) of the operating system
+on which the executable runs, unless that component itself accompanies the
+executable.
+
+If distribution of executable or object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the source
+code from the same place counts as distribution of the source code, even though
+third parties are not compelled to copy the source along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy, modify,
+sublicense or distribute the Program is void, and will automatically terminate
+your rights under this License. However, parties who have received copies, or
+rights, from you under this License will not have their licenses terminated so long
+as such parties remain in full compliance.
+
+5. You are not required to accept this License, since you have not signed it.
+However, nothing else grants you permission to modify or distribute the Program
+or its derivative works. These actions are prohibited by law if you do not accept
+this License. Therefore, by modifying or distributing the Program (or any work
+based on the Program), you indicate your acceptance of this License to do so,
+and all its terms and conditions for copying, distributing or modifying the
+Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the Program),
+the recipient automatically receives a license from the original licensor to copy,
+distribute or modify the Program subject to these terms and conditions. You
+may not impose any further restrictions on the recipients' exercise of the rights
+granted herein. You are not responsible for enforcing compliance by third parties
+to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent infringement
+or for any other reason (not limited to patent issues), conditions are imposed on
+you (whether by court order, agreement or otherwise) that contradict the
+conditions of this License, they do not excuse you from the conditions of this
+License. If you cannot distribute so as to satisfy simultaneously your obligations
+under this License and any other pertinent obligations, then as a consequence
+you may not distribute the Program at all. For example, if a patent license would
+not permit royalty-free redistribution of the Program by all those who receive
+copies directly or indirectly through you, then the only way you could satisfy
+both it and this License would be to refrain entirely from distribution of the
+Program.
+
+If any portion of this section is held invalid or unenforceable under any particular
+circumstance, the balance of the section is intended to apply and the section as
+a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or other
+property right claims or to contest validity of any such claims; this section has
+the sole purpose of protecting the integrity of the free software distribution
+system, which is implemented by public license practices. Many people have
+made generous contributions to the wide range of software distributed through
+that system in reliance on consistent application of that system; it is up to the
+author/donor to decide if he or she is willing to distribute software through any
+other system and a licensee cannot impose that choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain countries
+either by patents or by copyrighted interfaces, the original copyright holder who
+places the Program under this License may add an explicit geographical
+distribution limitation excluding those countries, so that distribution is permitted
+only in or among countries not thus excluded. In such case, this License
+incorporates the limitation as if written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions of the
+General Public License from time to time. Such new versions will be similar in
+spirit to the present version, but may differ in detail to address new problems or
+concerns.
+
+Each version is given a distinguishing version number. If the Program specifies a
+version number of this License which applies to it and "any later version", you
+have the option of following the terms and conditions either of that version or of
+any later version published by the Free Software Foundation. If the Program does
+not specify a version number of this License, you may choose any version ever
+published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software Foundation,
+write to the Free Software Foundation; we sometimes make exceptions for this.
+Our decision will be guided by the two goals of preserving the free status of all
+derivatives of our free software and of promoting the sharing and reuse of
+software generally.
+
+NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS
+NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE
+COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
+"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
+IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE,
+YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED
+TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY
+WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS
+PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
+(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY
+OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
Index: /branches/eam_branches/neb-host-20250516/MANIFEST
===================================================================
--- /branches/eam_branches/neb-host-20250516/MANIFEST	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/MANIFEST	(revision 42864)
@@ -0,0 +1,53 @@
+Build.PL
+Changes
+LICENSE
+MANIFEST
+README
+bin/neb-voladd
+bin/neb-cabadd
+bin/neb-voladm
+bin/neb-insedit
+bin/neb-host
+bin/nebdiskd
+docs/database_setup.txt
+docs/design.txt
+docs/install.txt
+docs/requirements.txt
+examples/Makefile
+examples/nebexample.c
+examples/nebexample.pl
+examples/uri_test.pl
+init.d/nebdiskd
+lib/Nebulous/Key.pm
+lib/Nebulous/Server.pm
+lib/Nebulous/Server.pod
+lib/Nebulous/Server/Apache.pm
+lib/Nebulous/Server/Config.pm
+lib/Nebulous/Server/Log.pm
+lib/Nebulous/Server/SOAP.pm
+lib/Nebulous/Server/SQL.pm
+lib/Test/Nebulous.pm
+scripts/bench_test.pl
+scripts/nebulous.cgi
+scripts/ptest.pl
+t/00_distribution.t 
+t/01_load.t
+t/02_config.t
+t/03_server_setup.t
+t/04_server_create_object.t
+t/05_server_replicate_object.t
+t/06_server_lock_object.t
+t/07_server_unlock_object.t
+t/08_server_find_instances.t
+t/09_server_delete_instance.t
+t/10_server_stat_object.t
+t/11_server_is_valid_volume_name.t
+t/12_server_is_valid_object_key.t
+t/13_server_find_objects.t
+t/14_server_rename_object.t
+t/15_server_xattr.t
+t/16_mounts.t
+t/17_server_swap_objects.t
+t/18_server_chmod_object.t
+t/19_server_prune_object.t
+t/75_parse_neb_key.t
Index: /branches/eam_branches/neb-host-20250516/README
===================================================================
--- /branches/eam_branches/neb-host-20250516/README	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/README	(revision 42864)
@@ -0,0 +1,16 @@
+pod2text Poi::PixelData.pm > README
+
+If this is still here it means the programmer was too lazy to create the readme file.
+
+You can create it now by using the command shown above from this directory.
+
+At the very least you should be able to use this set of instructions
+to install the module...
+
+perl Build.PL
+./Build
+./Build test
+./Build install
+
+
+If you are on a windows box you should use 'nmake' rather than 'make'.
Index: /branches/eam_branches/neb-host-20250516/autogen.sh
===================================================================
--- /branches/eam_branches/neb-host-20250516/autogen.sh	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/autogen.sh	(revision 42864)
@@ -0,0 +1,1 @@
+perl Build.PL -apxs /usr/sbin/apxs2 
Index: /branches/eam_branches/neb-host-20250516/bin/neb-admin
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-admin	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-admin	(revision 42864)
@@ -0,0 +1,982 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2005-2008  Joshua Hoblitt
+#
+# $Id: neb-admin,v 1.14 2008-10-16 22:24:39 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.02';
+
+use DBI;
+use Net::Server::Daemonize qw( check_pid_file create_pid_file unlink_pid_file );
+use URI;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my (
+    $db,
+    $dbhost,
+    $dbpass,
+    $dbuser,
+    $so_id_start,
+    $so_id_range,
+    $limit,
+    $offset,
+    $pending,
+    $balance,
+    $retarget,
+    $balance_N_sources,
+    $balance_M_destinations,
+    $removal,
+    $verbose,
+);
+
+$db     = $ENV{'NEB_DB'};
+$dbhost = $ENV{'NEB_DBHOST'} || 'localhost';
+$dbuser = $ENV{'NEB_USER'};
+$dbpass = $ENV{'NEB_PASS'};
+
+$offset = 0;
+
+GetOptions(
+    'db|d=s'                => \$db,
+    'host=s'                => \$dbhost,
+    'user|u=s'              => \$dbuser,
+    'pass|p=s'              => \$dbpass,
+    'pendingreplicate|r'    => \$pending,
+    'pendingbalance|b'      => \$balance,
+    'pendingtarget|t'       => \$retarget,
+    'balancesources|N=s'      => \$balance_N_sources,
+    'balancedestinations|M=s' => \$balance_M_destinations,
+#    'pendingremoval'        => \$removal,
+    'so_id_start=i'         => \$so_id_start,
+    'so_id_range=i'         => \$so_id_range,
+    'limit|l=i'             => \$limit,
+    'offset=i'              => \$offset,
+    'verbose|v'             => \$verbose,
+) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --db --user --pass", -exitval => 2 )
+    unless $db && $dbuser && $dbpass;
+pod2usage( -msg => "--limit is meaningless without --pendingreplicate",
+        -exitval => 2 )
+    if defined $limit and not (defined $pending or defined $balance or defined $retarget);
+pod2usage( -msg => "no operation specified", -exitval => 2 )
+    unless defined $pending or defined $balance or defined $retarget;
+pod2usage( -msg => "--pendingbalance needs --balancesources and --balancedestinations options",
+	   -exitval => 2)
+    if defined $balance and not (defined $balance_N_sources or defined $balance_M_destinations);
+# set default limit to 5
+$limit ||= 5;
+
+# check to make sure that only one instance of neb-admin is running
+# XXX this implies we should move pending replicate elsewhere
+my $pidfile;
+if (defined $pending) {
+    $pidfile = '/var/tmp/neb-admin.replicate';
+}
+elsif (defined $balance) {
+    $pidfile = '/var/tmp/neb-admin.balance';
+}
+elsif (defined $retarget) {
+    $pidfile = '/var/tmp/neb-admin.retarget';
+}
+else {
+    $pidfile = '/var/tmp/neb-admin';
+}
+# abort if an instance is already running
+END { unlink_pid_file($pidfile) };
+check_pid_file($pidfile);
+create_pid_file($pidfile);
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$db:host=$dbhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 1,
+    },
+);
+
+if ($pending) {
+    pending();
+} elsif ($removal) {
+    removal();
+} elsif ($balance) {
+    balance();
+} elsif ($retarget) {
+    retarget();
+} else {
+    die "THIS SHOULD NOT HAPPEN";
+}
+
+sub pending
+{
+# so_id, ext_id, instances, available_instances, need_recovery, recoverable
+# XXX don't remove the temp table code -- testing w/o it
+#    $dbh->do("CREATE TEMPORARY TABLE mymountedvol LIKE mountedvol");
+#    $dbh->do("INSERT INTO mymountedvol SELECT * FROM mountedvol");
+
+    if (not defined $so_id_start) { $so_id_start = 0; }
+    if (not defined $so_id_range) { $so_id_range = 100000; }
+    my $so_id_end = $so_id_start + $so_id_range;
+
+    # XXX check if so_id_start is beyond MAX(so_id): if so, exit with exit status 10
+    { 
+        my $query = $dbh->prepare("SELECT MAX(so_id) from storage_object");
+        $query->execute;
+        my $answer = $query->fetchrow_arrayref;
+        my $max_so_id = $$answer[0];
+        $query->finish;
+
+        if ($so_id_start > $max_so_id) { 
+            print STDERR "at end of so_id range, reset please\n";
+            exit 10;
+        }
+    }
+        
+    my $query = $dbh->prepare(
+    "
+        SELECT
+            so.so_id,
+            so.ext_id,
+            count(ins_id) as instances,
+            mv.name as volume_name,
+            mv.host as volume_host,
+            count(mv.vol_id) as available_instances,
+            count(mv.vol_id) > 0 as recoverable,
+            xattr.value as copies
+        FROM storage_object AS so
+        LEFT JOIN storage_object_xattr AS xattr
+            ON so.so_id = xattr.so_id
+            AND xattr.name = 'user.copies'
+        JOIN instance AS i
+            ON so.so_id = i.so_id
+        JOIN mountedvol AS mv
+            USING(vol_id)
+        WHERE
+            mv.available = 1
+            AND so.so_id >= $so_id_start
+            AND so.so_id <  $so_id_end
+        GROUP BY so_id
+        HAVING available_instances != instances OR instances != copies
+        LIMIT $limit"
+    );
+
+    $query->execute;
+
+    my @rows;
+    while (my $row = $query->fetchrow_hashref) {
+        push @rows, $row;        
+    }
+    $query->finish;
+        
+    exit unless scalar @rows;
+    
+    # compare number of responses to limit below
+    my $Npending = @rows;
+
+    print "replicatePending MULTI\n\n";
+
+    foreach my $obj (@rows) {
+        if (defined $verbose) {
+            require Data::Dumper;
+            print Data::Dumper::Dumper($obj);
+        }
+
+        my $so_id = $obj->{so_id};
+        my $key = $obj->{ext_id};
+        my $has = $obj->{instances};
+        my $vol_name = $obj->{volume_name};
+        my $vol_host = $obj->{volume_host};
+	my $available = $obj->{available_instances} || 0;
+        my $need_recovery = $obj->{need_recovery};
+        my $recoverable = $obj->{recoverable};
+        # if the copies xattr is unset and the object has 2 or more instances, we
+        # will assume a target of 2 copies
+        
+        # XXX change this: there should be no default value or we will
+        # have a race condition.  user.copies should never get set by
+        # any client until AFTER a file is no longer in use.
+        my $copies = $obj->{copies} || 2;
+
+        # objects with only a single instance that are offline are unrecoverable so
+        # we don't need to handle that special case
+#    next unless $need_recovery;
+        unless ($recoverable) {
+            warn "so_id: $so_id key: \'$key\' ",
+                "can not be recovered - no available instances\n";
+            next;
+        }
+
+        my $need = $copies - $available;
+	my $cmd = 'neb-stat';
+#        $need = 0 if $need < 0;
+        next unless $need;
+
+	if ($need > 0) {
+	    $cmd = 'neb-replicate';
+	}
+	elsif ($need < 0) {
+	    $cmd = 'neb-cull';
+	}
+
+        my %md = (
+            key         => $key,
+	    command     => $cmd,
+            need_copies => $need,
+            volume_name => $vol_name,
+            volume_host => $vol_host,
+
+#	    has         => $has,
+	    available   => $available,
+	    xattr_copies      => $copies,
+#	    recoverable => $recoverable,
+#	    need_recovery => $need_recovery,
+	    
+        );
+#	print "$key $cmd $need $vol_name $vol_host\n";
+        print_metadata("replicatePending", \%md);
+        print "\n";
+    }
+
+    # use a different exit status if we hit the limit (likely more files pending)
+    # This is a better way to do this.
+    if ($Npending == 0) {
+	exit 0;
+    }
+    else {
+	exit 1;
+    }
+
+#     if ($Npending == $limit) {
+#         exit 1;
+#     } 
+#     exit 0;
+}
+
+sub balance
+{
+# so_id, ext_id, instances, available_instances, need_recovery, recoverable
+# XXX don't remove the temp table code -- testing w/o it
+#    $dbh->do("CREATE TEMPORARY TABLE mymountedvol LIKE mountedvol");
+#    $dbh->do("INSERT INTO mymountedvol SELECT * FROM mountedvol");
+
+    if (not defined $so_id_start) { $so_id_start = 0; }
+    if (not defined $so_id_range) { $so_id_range = 100000; }
+    my $so_id_end = $so_id_start + $so_id_range;
+
+    # XXX check if so_id_start is beyond MAX(so_id): if so, exit with exit status 10
+    { 
+        my $query = $dbh->prepare("SELECT MAX(so_id) from storage_object");
+        $query->execute;
+        my $answer = $query->fetchrow_arrayref;
+        my $max_so_id = $$answer[0];
+        $query->finish;
+
+        if ($so_id_start > $max_so_id) { 
+            print STDERR "at end of so_id range, reset please\n";
+            exit 10;
+        }
+    }
+
+    # Calculate the average disk usage
+    my $average = 0;
+    my $avg_query = $dbh->prepare(
+	"select sum(used)/sum(total) AS average from mountedvol WHERE available = 1 AND allocate = 1"
+	);
+    $avg_query->execute();
+    while (my $row = $avg_query->fetchrow_hashref) {
+	$average = $row->{average};
+    }
+    $avg_query->finish;
+    print STDERR "average: $average\n";
+    if ($average == 0) {
+	exit 1;
+    }
+    # I truly am sorry for this statement.
+    my $query = $dbh->prepare(
+	"
+SELECT * FROM (
+#  -- U Randomize the list of all possible source/destination matches,then group by so_id to select one at random
+  SELECT T.*,volume.cab_id AS source_cab_id, volume.name AS source_name, volume.host AS source_host,
+         destination.vol_id AS destination_vol_id,destination.cab_id AS destination_cab_id,
+         destination.name AS destination_name,destination.host AS destination_host FROM (
+    -- V Select the volume that matches the instance with the largest ins_id
+    SELECT S.*,vol_id FROM (
+      -- Select objects that are in our so_id range, with two copies (and two requested copies)
+      SELECT MAX(ins_id) AS MAXins_id,MIN(ins_id) AS MINins_id,so_id,ext_id FROM
+       storage_object JOIN storage_object_xattr USING(so_id) RIGHT JOIN instance USING(so_id)
+       WHERE name = 'user.copies' AND value >= 2
+       AND ext_id LIKE '%ota%fits'                  -- This line is the emergency speedup option
+       AND so_id >= $so_id_start                    -- so_id_start
+       AND so_id <  $so_id_end                      -- so_id_end
+       GROUP BY so_id
+      ) AS S
+      JOIN instance ON (S.MAXins_id = instance.ins_id)
+      WHERE vol_id IN (
+       -- Y Extra select for debugging purposes
+       SELECT vol_id FROM (
+        -- Z Ran hosts by R (need to be a source) and return top N
+        SELECT mountedvol.vol_id,mountedvol.name,mountedvol.available,mountedvol.allocate,total,used,
+               (used / total) AS D,total * ((used / total) - $average ) AS R,  -- average
+               (used / total) - $average AS delta,
+               cab_id FROM
+        mountedvol JOIN volume USING(vol_id)
+        WHERE mountedvol.available = 1
+        AND mountedvol.xattr = 0
+        ORDER BY delta DESC LIMIT $balance_N_sources                                            -- N
+        ) AS ranked_sources
+        -- Z End
+       )
+       -- Y End
+       AND MAXins_id != MINins_id
+     ) AS T
+     -- V End
+     JOIN volume on T.vol_id = volume.vol_id RIGHT JOIN (
+       -- Y2 Chose a random destination from the list
+       SELECT vol_id,cab_id,name,host FROM (
+         -- Z2 Reverse rank hosts by R and return top M
+         SELECT mountedvol.vol_id,mountedvol.name,mountedvol.host,mountedvol.available,mountedvol.allocate,total,used,
+                (used / total) AS D,total * ((used / total) - $average ) AS R,  -- average
+                (used / total) - $average AS delta,
+                cab_id FROM
+         mountedvol JOIN volume USING(vol_id)
+         WHERE mountedvol.available = 1 AND mountedvol.allocate = 1 
+         AND mountedvol.xattr = 1
+         ORDER BY delta ASC LIMIT $balance_M_destinations                                               -- M
+       ) AS ranked_destinations
+       -- Z2 End
+     ORDER BY RAND()
+     ) AS destination
+     -- Y2 End
+   ON destination.cab_id != volume.cab_id
+   ORDER BY RAND()
+) AS results
+WHERE so_id IS NOT NULL
+-- U End
+GROUP BY so_id
+  LIMIT $limit                                               -- limit
+  OFFSET $offset                                             -- offset
+
+");
+
+#     my $query = $dbh->prepare(
+# 	"
+# SELECT * FROM (
+#  -- U Randomize the list of all possible source/destination matches,then group by so_id to select one at random
+#  SELECT T.*,volume.cab_id AS source_cab_id,volume.name AS source_name,volume.host AS source_host,
+#         destination.vol_id AS destination_vol_id,destination.cab_id AS destination_cab_id,
+#         destination.name AS destination_name,destination.host AS destination_host FROM (
+#   -- V Ensure that the copy we found on the source volume is the second copy instance
+#   SELECT K.vol_id AS here,K.ins_id,K.so_id,ext_id,value AS user_copies,MAXins_id,instance.vol_id AS there FROM (
+#    -- W Determine what the second copy instance is
+#    SELECT V.*,MAX(instance.ins_id) AS MAXins_id FROM (
+#     -- X Determine which hosts have instances of an object
+#     SELECT vol_id,ins_id,so_id,ext_id,value FROM 
+#     storage_object JOIN storage_object_xattr USING(so_id) JOIN instance USING(so_id)
+#     WHERE vol_id IN (
+#      -- Y Extra select for debugging purposes
+#      SELECT vol_id FROM (
+#       -- Z Ran hosts by R (need to be a source) and return top N
+#       SELECT mountedvol.vol_id,mountedvol.name,mountedvol.available,mountedvol.allocate,total,used,
+#              (used / total) AS D,total * ((used / total) - $average ) AS R,  -- average
+#              (used / total) - $average AS delta,
+#              cab_id FROM
+#       mountedvol JOIN volume USING(vol_id)
+#       WHERE mountedvol.available = 1
+#       ORDER BY delta DESC LIMIT $balance_N_sources                                            -- N
+#       ) AS ranked_sources
+#       -- Z End
+#      )
+#      -- Y End
+#     AND name = 'user.copies' AND value >= 2
+#   AND ext_id LIKE '%ota%fits'       -- This line is the emergency speedup option
+#     AND so_id >= $so_id_start                                  -- so_id_start
+#     AND so_id <  $so_id_end                                    -- so_id_end
+#    ) AS V -- volumes that host a copy
+#    -- X End
+#   LEFT OUTER JOIN instance USING(so_id) GROUP BY so_id
+#   ) AS K -- copies that are the second copy
+#   -- W End
+#  JOIN instance ON MAXins_id = instance.ins_id GROUP BY so_id
+#  ) AS T -- matched copies that I know the second copy is on my source volume
+#  -- V End
+#  JOIN volume on T.here = volume.vol_id RIGHT JOIN (
+#   -- Y2 Chose a random destination from the list
+#   SELECT vol_id,cab_id,name,host FROM (
+#    -- Z2 Reverse rank hosts by R and return top M
+#    SELECT mountedvol.vol_id,mountedvol.name,mountedvol.host,mountedvol.available,mountedvol.allocate,total,used,
+#           (used / total) AS D,total * ((used / total) - $average ) AS R,  -- average
+#           (used / total) - $average AS delta,
+#           cab_id FROM
+#    mountedvol JOIN volume USING(vol_id)
+#    WHERE mountedvol.available = 1 AND mountedvol.allocate = 1
+#    ORDER BY delta ASC LIMIT $balance_M_destinations                                               -- M
+#   ) AS ranked_destinations
+#   -- Z2 End
+#  ORDER BY RAND()
+#  ) AS destination
+#  -- Y2 End
+#  ON destination.cab_id != volume.cab_id
+#  WHERE here = there
+#  ORDER BY RAND()
+# ) AS results
+# -- U End
+# GROUP BY so_id
+#     LIMIT $limit                                               -- limit
+#     OFFSET $offset                                             -- offset
+
+# "    );
+
+    $query->execute();
+		    
+		    
+
+    my @rows;
+    while (my $row = $query->fetchrow_hashref) {
+        push @rows, $row;        
+    }
+    $query->finish;
+
+    print STDERR "No rows found\n" unless scalar @rows;
+    exit unless scalar @rows;
+    
+    # compare number of responses to limit below
+    my $Npending = @rows;
+
+    print "balancePending MULTI\n\n";
+
+    foreach my $obj (@rows) {
+        if (defined $verbose) {
+            require Data::Dumper;
+            print Data::Dumper::Dumper($obj);
+        }
+	
+
+#	my $here = $obj->{here};
+#	my $there = $obj->{there};
+#	my $ins_id = $obj->{ins_id};
+	my $max_ins_id = $obj->{MAXins_id};
+	
+	my $so_id = $obj->{so_id};
+	my $key   = $obj->{ext_id};
+#	my $copies = $obj->{user_copies};
+
+	my $source_vol_id = $obj->{vol_id};
+	my $source_cab_id = $obj->{source_cab_id};
+	my $source_name   = $obj->{source_name};
+	my $source_host   = $obj->{source_host};
+
+	my $destination_vol_id = $obj->{destination_vol_id};
+	my $destination_cab_id = $obj->{destination_cab_id};
+	my $destination_name   = $obj->{destination_name};
+	my $destination_host   = $obj->{destination_host};
+	
+#	unless ($here == $there) { $Npending-- ; next; }
+#	unless ($ins_id == $max_ins_id) { $Npending-- ; next; }
+	unless ($source_cab_id != $destination_cab_id) { $Npending-- ;  next; }
+	
+        my %md = (
+	    key           => $key,
+#	    copies        => $copies,
+	    so_id         => $so_id,
+	    source_vol_id => $source_vol_id,
+	    source_cab_id => $source_cab_id,
+	    source_name   => $source_name,
+	    source_host   => $source_host,
+	    destination_vol_id => $destination_vol_id,
+	    destination_cab_id => $destination_cab_id,
+	    destination_name   => $destination_name,
+	    destination_host   => $destination_host,
+#             key         => $key,
+# 	    command     => $cmd,
+#             need_copies => $need,
+#             volume_name => $vol_name,
+#             volume_host => $vol_host,
+
+# #	    has         => $has,
+# 	    available   => $available,
+# 	    xattr_copies      => $copies,
+# #	    recoverable => $recoverable,
+# #	    need_recovery => $need_recovery,
+	    
+        );
+#	print "$key $cmd $need $vol_name $vol_host\n";
+        print_metadata("balancePending", \%md);
+        print "\n";
+    }
+
+    # use a different exit status if we hit the limit (likely more files pending)
+    print STDERR "Which exit: $Npending $limit\n";
+    # This is a better way to do this. This should be even better-er.
+    if ($Npending < $limit) {
+	exit 0;
+    }
+    else {
+	exit 1;
+    }
+
+#     if ($Npending == $limit) {
+#         exit 1;
+#     } 
+#     exit 0;
+}
+
+sub retarget
+{
+    my %destination;
+    my %vol_id_list = ('ipp004.0' => 1,'ipp005.0' => 2, 'ipp006.0' => 3, 'ipp007.0' => 4,'ipp008.0' => 50,'ipp009.0' => 6,'ipp010.0' => 7,
+		       'ipp011.0' => 8,'ipp012.0' => 15,'ipp013.0' => 16, 'ipp014.0' => 18,'ipp015.0' => 17,'ipp016.0' => 9,'ipp017.0' => 10,'ipp018.0' => 11,'ipp019.0' => 12,
+		       'ipp020.0' => 52,'ipp021.0' => 14,'ipp023.0' => 19,'ipp024.0' => 20,'ipp025.0' => 32,'ipp026.0' => 21, 'ipp027.0' => 49,
+		       'ipp028.0' => 23,'ipp029.0' => 24,'ipp030.0' => 25,'ipp031.0' => 26,'ipp032.0' => 27,'ipp033.0' => 28,'ipp034.0' => 29,
+		       'ipp035.0' => 30,'ipp036.0' => 31,'ipp037.0' => 51,'ipp038.0' => 33,'ipp039.0' => 34,'ipp040.0' => 35,'ipp041.0' => 36,
+		       'ipp042.0' => 37,'ipp043.0' => 38,'ipp044.0' => 39,'ipp045.0' => 40,'ipp046.0' => 41,'ipp047.0' => 42,'ipp048.0' => 43,
+		       'ipp049.0' => 44,'ipp050.0' => 45,'ipp051.0' => 46,'ipp052.0' => 47,'ipp053.0' => 48,'ipp054.0' => 62,'ipp055.0' => 63,
+		       'ipp056.0' => 64,'ipp057.0' => 65,'ipp058.0' => 66,'ipp059.0' => 67,'ipp060.0' => 68,'ipp061.0' => 69,'ipp062.0' => 70,
+		       'ipp063.0' => 71,'ipp064.0' => 72,'ipp065.0' => 73,'ipp066.0' => 74);
+# This is replaced below.
+#     %{ $destination{VOL} }  = ('ota01' => 7,       'ota02' => 8,       'ota03' => 3,       'ota04' => 3,       'ota05' => 4,       'ota06' => 4,
+# 			       'ota10' => 50,      'ota11' => 50,      'ota12' => 6,       'ota13' => 6,       'ota14' => 7,       'ota15' => 8,       'ota16' => 15,      'ota17' => 15,
+# 			       'ota20' => 16,      'ota21' => 18,      'ota22' => 18,      'ota23' => 17,      'ota24' => 9,       'ota25' => 9,       'ota26' => 10,      'ota27' => 11,
+# 			       'ota30' => 12,      'ota31' => 52,      'ota32' => 14,      'ota33' => 14,      'ota34' => 19,      'ota35' => 16,      'ota36' => 20,      'ota37' => 12,
+# 			       'ota40' => 32,      'ota41' => 11,      'ota42' => 10,      'ota43' => 17,      'ota44' => 49,      'ota45' => 23,      'ota46' => 24,      'ota47' => 25,
+# 			       'ota50' => 26,      'ota51' => 27,      'ota52' => 28,      'ota53' => 29,      'ota54' => 30,      'ota55' => 31,      'ota56' => 51,      'ota57' => 33,
+# 			       'ota60' => 34,      'ota61' => 35,      'ota62' => 36,      'ota63' => 37,      'ota64' => 38,      'ota65' => 39,      'ota66' => 41,      'ota67' => 42,
+# 			       'ota71' => 43,      'ota72' => 44,      'ota73' => 45,      'ota74' => 46,      'ota75' => 47,      'ota76' => 48);
+    %{ $destination{HOST} } = ('ota01' => 'ipp010','ota02' => 'ipp011','ota03' => 'ipp006','ota04' => 'ipp054','ota05' => 'ipp007','ota06' => 'ipp055',
+			       'ota10' => 'ipp008','ota11' => 'ipp056','ota12' => 'ipp009','ota13' => 'ipp057','ota14' => 'ipp058','ota15' => 'ipp059','ota16' => 'ipp012','ota17' => 'ipp060',
+			       'ota20' => 'ipp013','ota21' => 'ipp014','ota22' => 'ipp061','ota23' => 'ipp015','ota24' => 'ipp016','ota25' => 'ipp062','ota26' => 'ipp064','ota27' => 'ipp065',
+			       'ota30' => 'ipp066','ota31' => 'ipp020','ota32' => 'ipp063','ota33' => 'ipp021','ota34' => 'ipp023','ota35' => 'ipp013','ota36' => 'ipp024','ota37' => 'ipp019',
+			       'ota40' => 'ipp025','ota41' => 'ipp018','ota42' => 'ipp017','ota43' => 'ipp015','ota44' => 'ipp027','ota45' => 'ipp028','ota46' => 'ipp029','ota47' => 'ipp030',
+			       'ota50' => 'ipp031','ota51' => 'ipp032','ota52' => 'ipp033','ota53' => 'ipp034','ota54' => 'ipp035','ota55' => 'ipp036','ota56' => 'ipp037','ota57' => 'ipp038',
+			       'ota60' => 'ipp039','ota61' => 'ipp040','ota62' => 'ipp041','ota63' => 'ipp042','ota64' => 'ipp043','ota65' => 'ipp044','ota66' => 'ipp046','ota67' => 'ipp047',
+			       'ota71' => 'ipp048','ota72' => 'ipp049','ota73' => 'ipp050','ota74' => 'ipp051','ota75' => 'ipp052','ota76' => 'ipp053');
+    %{ $destination{NAME} } = ('ota01' => 'ipp010.0','ota02' => 'ipp011.0','ota03' => 'ipp006.0','ota04' => 'ipp054.0','ota05' => 'ipp007.0','ota06' => 'ipp055.0',
+			       'ota10' => 'ipp008.0','ota11' => 'ipp056.0','ota12' => 'ipp009.0','ota13' => 'ipp057.0','ota14' => 'ipp058.0','ota15' => 'ipp059.0','ota16' => 'ipp012.0','ota17' => 'ipp060.0',
+			       'ota20' => 'ipp013.0','ota21' => 'ipp014.0','ota22' => 'ipp061.0','ota23' => 'ipp015.0','ota24' => 'ipp016.0','ota25' => 'ipp062.0','ota26' => 'ipp064.0','ota27' => 'ipp065.0',
+			       'ota30' => 'ipp066.0','ota31' => 'ipp020.0','ota32' => 'ipp063.0','ota33' => 'ipp021.0','ota34' => 'ipp023.0','ota35' => 'ipp013.0','ota36' => 'ipp024.0','ota37' => 'ipp019.0',
+			       'ota40' => 'ipp025.0','ota41' => 'ipp018.0','ota42' => 'ipp017.0','ota43' => 'ipp015.0','ota44' => 'ipp027.0','ota45' => 'ipp028.0','ota46' => 'ipp029.0','ota47' => 'ipp030.0',
+			       'ota50' => 'ipp031.0','ota51' => 'ipp032.0','ota52' => 'ipp033.0','ota53' => 'ipp034.0','ota54' => 'ipp035.0','ota55' => 'ipp036.0','ota56' => 'ipp037.0','ota57' => 'ipp038.0',
+			       'ota60' => 'ipp039.0','ota61' => 'ipp040.0','ota62' => 'ipp041.0','ota63' => 'ipp042.0','ota64' => 'ipp043.0','ota65' => 'ipp044.0','ota66' => 'ipp046.0','ota67' => 'ipp047.0',
+			       'ota71' => 'ipp048.0','ota72' => 'ipp049.0','ota73' => 'ipp050.0','ota74' => 'ipp051.0','ota75' => 'ipp052.0','ota76' => 'ipp053.0');
+    foreach my $k (keys %{ $destination{NAME} }) {
+	$destination{VOL}{$k} = $vol_id_list{$destination{NAME}{$k}};
+    }
+    my %blocked_volume = ('ipp027' => 1,
+			  'ipp037' => 1,
+			  'ipp043' => 1,
+			  'ipp044' => 1,
+			  'ipp053' => 1,
+			  'ipp023' => 1,'ipp024' => 1,'ipp025' => 1,'ipp026' => 1,'ipp028' => 1,'ipp029' => 1,'ipp030' => 1,'ipp031' => 1,'ipp032' => 1,'ipp033' => 1,
+			  'ipp034' => 1,'ipp035' => 1,'ipp036' => 1,'ipp038' => 1,'ipp039' => 1,'ipp040' => 1,'ipp041' => 1,'ipp042' => 1,'ipp045' => 1,'ipp046' => 1,
+			  'ipp047' => 1,'ipp048' => 1,'ipp049' => 1,'ipp050' => 1,'ipp051' => 1,'ipp052' => 1,
+			  # These hosts are blocked due to concern about their RAID arrays.
+#			  'ipp054' => 1,'ipp055' => 1,'ipp056' => 1,'ipp057' => 1,'ipp058' => 1,'ipp059' => 1,
+#			  'ipp060' => 1,'ipp061' => 1,'ipp062' => 1,'ipp063' => 1,'ipp064' => 1,'ipp065' => 1, 'ipp066' => 1
+#			  'ipp064' => 1, 'ipp066' => 1
+			  'ipp064' => 1
+	);
+    if (not defined $so_id_start) { $so_id_start = 0; }
+    if (not defined $so_id_range) { $so_id_range = 100000; }
+    my $so_id_end = $so_id_start + $so_id_range;
+
+    # XXX check if so_id_start is beyond MAX(so_id): if so, exit with exit status 10
+    { 
+        my $query = $dbh->prepare("SELECT MAX(so_id) from storage_object");
+        $query->execute;
+        my $answer = $query->fetchrow_arrayref;
+        my $max_so_id = $$answer[0];
+        $query->finish;
+
+        if ($so_id_start > $max_so_id) { 
+            print STDERR "at end of so_id range, reset please\n";
+            exit 10;
+        }
+    }
+
+    # Determine a list of first instances
+#     my $query = $dbh->prepare(
+# 	"
+# SELECT S.so_id,ext_id,ins_id,uri,substr(uri,-10,5) AS class_id,vol_id,name AS vol_name,host AS vol_host FROM (
+#   select * FROM (
+#   select MAX(ins_id) AS MAXins_id, MIN(ins_id) AS MINins_id, so_id,ext_id,value AS user_copies FROM storage_object 
+#     JOIN storage_object_xattr USING(so_id) 
+#     RIGHT JOIN instance USING(so_id) 
+#      WHERE name = 'user.copies' 
+#      AND value >= 2 
+#      AND ext_id LIKE '%ota%fits' 
+#      AND so_id >= $so_id_start
+#      AND so_id <  $so_id_end
+#      GROUP BY so_id 
+#   ) AS V 
+#   WHERE MAXins_id != MINins_id
+#   ) AS S
+#   JOIN instance ON (S.MINins_id = instance.ins_id AND S.so_id = instance.so_id) 
+#   JOIN volume USING(vol_id) WHERE volume.available = 1
+#   LIMIT $limit
+#   OFFSET $offset
+# ");
+    
+    print STDERR "This query is broken so I have disabled it to get the MHPCC->ATRC shuffle going.\n";
+    exit 0;
+    my $query;
+#     my $query = $dbh->prepare(
+# 	"
+# SELECT Z.so_id,ext_id,Z.ins_id,Z.uri,substr(Z.uri,-10,5) AS class_id,Z.vol_id,vol_name,vol_host,
+#        instance.vol_id AS  second_vol_id,volume.name AS second_name,volume.host AS second_host FROM (
+#   SELECT S.so_id,ext_id,ins_id,uri,substr(uri,-10,5) AS class_id,vol_id,name AS vol_name,host AS vol_host,MAXins_id FROM (
+#     select * FROM (
+#       select MAX(ins_id) AS MAXins_id, MIN(ins_id) AS MINins_id, so_id,ext_id FROM storage_object
+#         JOIN storage_object_xattr USING(so_id)
+#         RIGHT JOIN instance USING(so_id)
+#          WHERE name = 'user.copies'
+#          AND value >= 2
+#          AND ext_id LIKE '%ota%fits'
+#          AND so_id >= $so_id_start
+#          AND so_id <  $so_id_end
+#          GROUP BY so_id
+#       ) AS V
+#       WHERE MAXins_id != MINins_id
+#     ) AS S
+#     JOIN instance ON (S.MINins_id = instance.ins_id AND S.so_id = instance.so_id)
+#     JOIN volume USING(vol_id) WHERE volume.available = 1
+#     AND volume.name AS $requested_source
+#   ) AS Z
+# JOIN instance ON (MAXins_id = instance.ins_id AND Z.so_id = instance.so_id)
+# JOIN volume ON(instance.vol_id = volume.vol_id) WHERE volume.available = 1
+# LIMIT $limit
+# OFFSET $offset
+# ");
+
+    $query->execute();
+
+    my @rows;
+    while (my $row = $query->fetchrow_hashref) {
+        push @rows, $row;        
+    }
+    $query->finish;
+
+    print STDERR "No rows found\n" unless scalar @rows;
+    exit unless scalar @rows;
+    
+    # compare number of responses to limit below
+    my $Npending = @rows;
+
+    print "targetPending MULTI\n\n";
+
+    foreach my $obj (@rows) {
+        if (defined $verbose) {
+            require Data::Dumper;
+            print Data::Dumper::Dumper($obj);
+        }
+	
+	my $so_id = $obj->{so_id};
+	my $key   = $obj->{ext_id};
+
+	my $ins_id = $obj->{ins_id};
+	my $uri   = $obj->{uri};
+	my $class_id = $obj->{class_id};
+
+	my $source_vol_id = $obj->{vol_id};
+	my $source_name   = $obj->{vol_name};
+	my $source_host   = $obj->{vol_host};
+
+	my $second_vol_id   = $obj->{second_vol_id};
+	my $second_vol_name = $obj->{second_name};
+	my $second_vol_host = $obj->{second_host};
+
+	my $destination_vol_id = $destination{VOL}{$class_id};
+	my $destination_name   = $destination{NAME}{$class_id};
+	my $destination_host   = $destination{HOST}{$class_id};
+	my $destination_uri    = $uri;
+	unless ((defined($destination_uri))&&
+		(defined($source_name))&&
+		(defined($destination_name))) {
+	    next;
+	}
+	
+	$destination_uri =~ s/$source_name/$destination_name/;
+	
+        my %md = (
+	    so_id         => $so_id,
+	    key           => $key,
+	    ins_id        => $ins_id,
+	    uri           => $uri,
+	    class_id      => $class_id,
+	    
+	    source_vol_id => $source_vol_id,
+	    source_name   => $source_name,
+	    source_host   => $source_host,
+
+	    destination_vol_id => $destination_vol_id,
+	    destination_name   => $destination_name,
+	    destination_host   => $destination_host,
+	    destination_uri    => $destination_uri,
+	    second_host        => $second_vol_host,
+	    second_vol_id      => $second_vol_id
+#	    blocked_host_bool  => defined($blocked_volume{$destination_host})
+        );
+	
+	if ((defined($blocked_volume{$destination_host}))||
+	    ($source_vol_id == $destination_vol_id)||
+	    ($second_vol_id == $destination_vol_id)||
+	    ($source_name   eq $destination_name)||
+	    ($second_vol_name eq $destination_name)) {
+#	    print_metadata("targetFailed", \%md);
+	    next;
+	}
+
+	if ($source_vol_id != $destination_vol_id) {
+	    print_metadata("targetPending", \%md);
+	    print "\n";
+	}
+    }
+
+    # use a different exit status if we hit the limit (likely more files pending)
+    print STDERR "Which exit: $Npending $limit\n";
+    # This is a better way to do this. This should be even better-er.
+    if ($Npending < $limit) {
+	exit 0;
+    }
+    else {
+	exit 1;
+    }
+}
+
+sub removal
+{
+# so_id, ext_id, instances, available_instances, need_recovery, recoverable
+    $dbh->do("CREATE TEMPORARY TABLE mymountedvol LIKE mountedvol");
+    $dbh->do("INSERT INTO mymountedvol SELECT * FROM mountedvol");
+    $dbh->do("CREATE TEMPORARY TABLE myvolume LIKE volume");
+    $dbh->do("INSERT INTO myvolume SELECT * FROM volume");
+
+    my $query = $dbh->prepare(
+    );
+    $query->execute;
+
+    $dbh->do("DROP TABLE IF EXISTS mymountedvol");
+    $dbh->do("DROP TABLE IF EXISTS myvolume");
+
+    my @rows;
+    while (my $row = $query->fetchrow_hashref) {
+        push @rows, $row;        
+    }
+    $query->finish;
+        
+    exit unless scalar @rows;
+
+    print "removalPending MULTI\n\n";
+
+    foreach my $obj (@rows) {
+        if (defined $verbose) {
+            require Data::Dumper;
+            print Data::Dumper::Dumper($obj);
+        }
+
+        my $so_id = $obj->{so_id};
+        my $key = $obj->{ext_id};
+        my $has = $obj->{instances};
+        my $vol_name = $obj->{volume_name};
+        my $vol_host = $obj->{volume_host};
+        my $available = $obj->{available_instances} || 0;
+        my $need_recovery = $obj->{need_recovery};
+        my $recoverable = $obj->{recoverable};
+        # if the copies xattr is unset and the object has 2 or more instances, we
+        # will assume a target of 2 copies
+        my $copies = $obj->{copies} || 2;
+
+        # objects with only a single instance that are offline are unrecoverable so
+        # we don't need to handle that special case
+#    next unless $need_recovery;
+        unless ($recoverable) {
+            warn "so_id: $so_id key: \'$key\' ",
+                "can not be recovered - no available instances\n";
+            next;
+        }
+
+        my $need = $copies - $available;
+        $need = 0 if $need < 0;
+
+        next unless $need;
+
+        my %md = (
+            key         => $key,
+            need_copies => $need,
+            volume_name => $vol_name,
+            volume_host => $vol_host,
+        );
+
+        print_metadata("replicatePending", \%md);
+        print "\n";
+    }
+
+    return 1;
+}
+
+
+sub print_metadata
+{
+    my ($name, $pairs) = @_;
+
+    print "$name METADATA\n";
+
+    # format from formatMetadataItem() in pzMetadataConfig.c
+    foreach my $key (keys %$pairs) {
+        my $value = $pairs->{$key};
+        if (looks_like_number($value)) {
+            printf("   " . "%-15s  %-8s  %-15s\n", $key, "S32", $value);
+        } else {
+            printf("   " . "%-15s  %-8s  %-15s\n", $key, "STR", $value);
+        }
+    }
+    
+    print "END\n";
+}
+
+# looks_like_number() was stolen from Scalar::Util
+#
+# Copyright (c) 1997-2006 Graham Barr <gbarr@pobox.com>. All rights reserved.
+# This program is free software; you can redistribute it and/or modify it
+# under the same terms as Perl itself.
+
+sub looks_like_number {
+  local $_ = shift;
+
+  # checks from perlfaq4
+  return 0 if !defined($_) or ref($_);
+  return 1 if (/^[+-]?\d+$/); # is a +/- integer
+  return 1 if (/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/); # a C float
+  return 1 if ($] >= 5.008 and /^(Inf(inity)?|NaN)$/i) or ($] >= 5.006001 and /^Inf$/i);
+
+  0;
+}
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-admin - perform misc. Nebulous administrative functions
+
+=head1 SYNOPSIS
+
+    neb-admin [--host <db host>] [--db <db name>] [--user <db username>]
+        [--pass <db password>] [--pendingreplicate] [--limit <n>]
+
+=head1 DESCRIPTION
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --pendingreplicate|-r
+
+Print out a list of nebulous keys that need additional replications created.
+
+=item * --limit|-l <n>
+
+Limits the number of items return in response to some request (like
+C<--pendingreplicate>). 
+
+Optional.  Defaults to 5.  --limit 0 is treated as asking for the default
+value.
+
+=item * --host <database host>
+
+Name of host on which the database resides.
+
+Optional.  Defaults to C<localhost>.
+
+=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.
+
+=back
+
+=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
+
+Just me, myself, and I.
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=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-addvol>, L<neb-fsck>, L<neb-initdb>, L<nebdiskd>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/bin/neb-aliasadd
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-aliasadd	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-aliasadd	(revision 42864)
@@ -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: /branches/eam_branches/neb-host-20250516/bin/neb-cabadd
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-cabadd	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-cabadd	(revision 42864)
@@ -0,0 +1,204 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2005-2008  Joshua Hoblitt
+#
+# $Id: neb-cabadd,v 1.2 2008-10-01 21:05:57 jhoblitt Exp $
+
+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, $cname, $location, $site_id, $update, $cab_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,
+    '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 --site_id", -exitval => 2 )
+    unless defined $db && defined $dbuser && defined $dbpass && defined $cname && defined $site_id;
+
+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 (defined($update)) {
+    unless (defined $cname && defined $location && defined $site_id && defined $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__
+
+=pod
+
+=head1 NAME
+
+neb-cabadd - add or modify a cabinet in Nebulous
+
+=head1 SYNOPSIS
+
+    neb-cabadd --cname <cabinet name> --location <cabinet location> --site_id <site_id> [--update]
+    	       [--db <database>] [--user <username>] [--pass <password>] [--host <hostname] 
+
+=head1 DESCRIPTION
+
+This program initialize a database for use by L<Nebulous::Server> by creating
+the appropriate set of tables.  Any pre-existing tables with conflicting names
+are first removed.
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --cname <cabinet name>
+
+Symbolic name of cabinet being added.
+
+=item * --location <cabinet location>
+
+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>
+
+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
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=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: /branches/eam_branches/neb-host-20250516/bin/neb-cabinet
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-cabinet	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-cabinet	(revision 42864)
@@ -0,0 +1,184 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.01';
+
+use DBI;
+use URI::file;
+use SQL::Interp qw( sql_interp );
+use URI;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my ($dbname, $dbhost, $dbpass, $dbuser, $debug);
+
+# $dbname = $ENV{'NEB_DB'} unless $db;
+# $dbhost = $ENV{'NEB_DBHOST'} || 'localhost';
+# $dbuser = $ENV{'NEB_USER'} unless $dbuser;
+# $dbpass = $ENV{'NEB_PASS'} unless $dbpass;
+
+# XXX this program (and neb-df) should not have a hard-wired nebulous config location
+# XXX can this be part of the build?
+
+# my $conffile = "/data/ippc64.1/ippitc/ippconfig/nebulous.config";
+# my $conffile = "/home/panstarrs/ipp/ippconfig/nebulous.config";
+my $conffile = "/home/panstarrs/ippps1/ippconfig/nebulous.config";
+
+if (-e $conffile) {
+    open(IN,$conffile);
+    while(<IN>) {
+	chomp;
+	if ($_ =~ /NEB_DB\s+/) {
+	    $dbname = (split /\s+/,$_)[2];
+	}
+	elsif ($_ =~ /NEB_DBHOST/) {
+	    $dbhost = (split /\s+/,$_)[2];
+	}
+	elsif ($_ =~ /NEB_USER/) {
+	    $dbuser = (split /\s+/,$_)[2];
+	}
+	elsif ($_ =~ /NEB_PASS/) {
+	    $dbpass = (split /\s+/,$_)[2];
+	}
+    }
+    close(IN);
+}
+
+GetOptions(
+    'dbname=s'       => \$dbname,
+    'dbhost=s'       => \$dbhost,
+    'dbuser=s'       => \$dbuser,
+    'dbpass=s'       => \$dbpass,
+    ) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Cannot configure nebulous database", -exitval => 2) unless
+    defined $dbname && defined $dbhost && defined $dbuser && defined $dbpass;
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$dbname:host=$dbhost", $dbuser, $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 0,
+    },
+    );
+
+my $sql_cabinets = "SELECT cab_id, name, site_id, location from cabinet";
+warn "$sql_cabinets\n" if $debug;
+
+my $rowref = $dbh->selectall_arrayref( $sql_cabinets );
+my @rowarr = @$rowref;
+
+foreach my $valref (@rowarr) {
+    my @values = @$valref;
+
+    # there should be exactly 4 values
+    my $Nvalues = @values;
+    if ($Nvalues != 4) { print "ERROR: invalid response?\n"; next; }
+    
+#   printf ("%s %s %s %s", $values[0], $values[1], $values[2], $values[3]);
+    printf ("%s %s %s %s", @values);
+}
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-cabinet - show / manage the state of Nebulous storage cabinets
+
+=head1 SYNOPSIS
+
+    neb-cabinet
+    [--dbhost <database host>] [--db <database name>]
+    [--dbuser <database user>] [--dbpass <database password>]
+
+=head1 DESCRIPTION
+
+This program displays a Nebulous server's list of cabinets.  
+
+=head1 OPTIONS
+
+=item * --dbname <database name>
+
+Name of database (C<namespace>) to create tables in.
+
+Optional if the appropriate environment variable is set.
+
+=item * --dbhost <database host>
+
+Host name where the database resides.
+
+Defaults to C<localhost>.
+
+=item * --dbuser <database username>
+
+Username to authenticate with.
+
+Optional if the appropriate environment variable is set.
+
+=item * --dbpass <database password>
+
+Password to authenticate with.
+
+Optional if the appropriate environment variable is set.
+
+=back
+
+=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 COPYRIGHT
+
+Copyright (C) 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-voladm>, L<neb-voladd>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/bin/neb-countcheck
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-countcheck	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-countcheck	(revision 42864)
@@ -0,0 +1,254 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2005-2008  Joshua Hoblitt
+#
+# $Id: neb-admin,v 1.14 2008-10-16 22:24:39 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.02';
+
+use DBI;
+use Net::Server::Daemonize qw( check_pid_file create_pid_file unlink_pid_file );
+use URI;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my (
+    $db,
+    $dbhost,
+    $dbpass,
+    $dbuser,
+    $so_id_start,
+    $so_id_range,
+    $limit,
+    $verbose,
+);
+
+$db     = $ENV{'NEB_DB'};
+$dbhost = $ENV{'NEB_DBHOST'} || 'localhost';
+$dbuser = $ENV{'NEB_USER'};
+$dbpass = $ENV{'NEB_PASS'};
+
+GetOptions(
+    'db|d=s'                => \$db,
+    'host=s'                => \$dbhost,
+    'user|u=s'              => \$dbuser,
+    'pass|p=s'              => \$dbpass,
+    'so_id_start=i'         => \$so_id_start,
+    'so_id_range=i'         => \$so_id_range,
+    'limit|l=i'             => \$limit,
+    'verbose|v'             => \$verbose,
+) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --db --user --pass", -exitval => 2 )
+    unless $db && $dbuser && $dbpass;
+
+# set default limit to 5
+#$limit ||= 5;
+
+# check to make sure that only one instance of neb-admin is running
+# XXX this implies we should move pending replicate elsewhere
+# my $pidfile = '/var/tmp/neb-admin';
+# # abort if an instance is already running
+# END { unlink_pid_file($pidfile) };
+# check_pid_file($pidfile);
+# create_pid_file($pidfile);
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$db:host=$dbhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 1,
+    },
+);
+
+scan();
+# if ($pending) {
+#     pending();
+# } elsif ($removal) {
+#     removal();
+# } else {
+#     die "THIS SHOULD NOT HAPPEN";
+# }
+
+sub scan {
+    if (not defined $so_id_start) { $so_id_start = 0; }
+    if (not defined $so_id_range) { $so_id_range = 100000; }
+    my $so_id_end = $so_id_start + $so_id_range;
+
+    # XXX check if so_id_start is beyond MAX(so_id): if so, exit with exit status 10
+    { 
+        my $query = $dbh->prepare("SELECT MAX(so_id) from storage_object");
+        $query->execute;
+        my $answer = $query->fetchrow_arrayref;
+        my $max_so_id = $$answer[0];
+        $query->finish;
+
+        if ($so_id_start > $max_so_id) { 
+            print STDERR "at end of so_id range, reset please\n";
+            exit 10;
+        }
+    }
+        
+    my $sth = 	"SELECT so_id,Nhave,Nwant,ext_id FROM 
+            (SELECT so_id,count(ins_id) AS Nhave,value AS Nwant FROM
+                instance 
+                JOIN storage_object_xattr USING(so_id)
+                JOIN mountedvol USING(vol_id)
+                WHERE storage_object_xattr.name = 'user.copies'
+                AND mountedvol.available = 1
+                AND so_id >= $so_id_start
+                AND so_id <  $so_id_end
+                GROUP BY so_id
+                HAVING Nhave != Nwant
+            ) AS V
+            JOIN storage_object USING(so_id)";
+    if ($limit) {
+	$sth .= " LIMIT $limit ";
+    }
+
+
+    my $query = $dbh->prepare($sth);
+    $query->execute;
+
+    my @rows;
+    while (my $row = $query->fetchrow_hashref) {
+	my $call;
+	if ($row->{Nhave} > $row->{Nwant}) {
+	    $call = 'neb-cull';
+	}
+	elsif ($row->{Nhave} < $row->{Nwant}) {
+	    $call = 'neb-replicate';
+	}
+	else {
+	    $call = 'neb-stat';
+	}
+	print "$row->{so_id} $row->{Nhave} $row->{Nwant} $call $row->{ext_id}\n";
+        push @rows, $row;        
+    }
+    $query->finish;
+        
+    exit unless scalar @rows;
+}
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-countcheck
+
+=head1 SYNOPSIS
+
+    neb-countcheck [--host <db host>] [--db <db name>] [--user <db username>]
+        [--pass <db password>] [--limit <n>] [--so_id_start <S>] [--so_id_range <N>]
+
+=head1 DESCRIPTION
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --limit|-l <n>
+
+Limits the number of items return in response to some request (like
+C<--pendingreplicate>). 
+
+Optional.  Defaults to 5.  --limit 0 is treated as asking for the default
+value.
+
+=item * --host <database host>
+
+Name of host on which the database resides.
+
+Optional.  Defaults to C<localhost>.
+
+=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.
+
+=back
+
+=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
+
+
+=head1 SUPPORT
+
+
+=head1 AUTHOR
+
+CZW
+
+=head1 COPYRIGHT
+
+Copyright (C) 2010 Chris Waters.  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-addvol>, L<neb-fsck>, L<neb-initdb>, L<nebdiskd>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/bin/neb-fsck
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-fsck	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-fsck	(revision 42864)
@@ -0,0 +1,224 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2005-2008  Joshua Hoblitt
+#
+# $Id: neb-fsck,v 1.2 2008-03-20 21:12:06 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.01';
+
+use DBI;
+use Nebulous::Server::SQL;
+use Nebulous::Client;
+use URI;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my ($db, $dbuser, $dbpass, $server);
+
+$db     = $ENV{'NEB_DB'} unless $db;
+$dbuser = $ENV{'NEB_USER'} unless $dbuser;
+$dbpass = $ENV{'NEB_PASS'} unless $dbpass;
+$server = $ENV{'NEB_SERVER'} unless $server;
+
+GetOptions(
+    'db=s'          => \$db,
+    'user=s'        => \$dbuser,
+    'pass=s'        => \$dbpass,
+    'server|s=s'    => \$server,
+) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --server", -exitval => 2 )
+    unless $server;
+pod2usage( -msg => "Required options: --db --user --pass", -exitval => 2 )
+    unless $db && $dbuser && $dbpass;
+
+my $neb = Nebulous::Client->new(
+    proxy => "$server",
+);
+
+die "can't connected to Nebulous Server: $server"
+    unless defined $neb;
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$db:host=localhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 1,
+    },
+);
+
+my $sql = Nebulous::Server::SQL->new();
+
+print "Pass 1: Look for objects with inaccessable instances\n";
+
+# so_id, ext_id, instances, available_instances, need_recovery, recoverable
+my $query = $dbh->prepare( $sql->find_objects_with_unavailable_instances );
+$query->execute;
+
+my @rows;
+while (my $row = $query->fetchrow_hashref) {
+    push @rows, $row;        
+}
+$query->finish;
+
+foreach my $obj (@rows) {
+#    use Data::Dumper;
+#    print Dumper($obj);
+    my $so_id = $obj->{so_id};
+    my $key = $obj->{ext_id};
+    my $has = $obj->{instances};
+    my $available = $obj->{available_instances} || 0;
+    my $need_recovery = $obj->{need_recovery};
+    my $recoverable = $obj->{recoverable};
+    # if the copies xattr is unset and the object has 2 or more instances, we
+    # will assume a target of 2 copies
+    my $copies = $obj->{copies} || 2;
+
+    # objects with only a single instance that are offline are unrecoverable so
+    # we don't need to handle that special case
+    next unless $need_recovery;
+    unless ($recoverable) {
+        warn "so_id: $so_id key: \'$key\' ",
+            "can not be recovered - no available instances\n";
+        next;
+    }
+
+    my $need = $copies - $available;
+    $need = 0 if $need < 0;
+
+    next unless $need;
+
+    for (; $need > 0; $need--) {
+        warn "so_id: $so_id key: \'$key\' ",
+            "has $available available instances, target $copies -- replicating";
+        $neb->replicate($obj->{ext_id})
+            or warn "so_id: $so_id key: \'$key\' replication failed";
+
+        $available++;
+    }
+}
+
+print "Pass 2: Check volumes for files without an instance\n";
+
+# get a list of volumes and their paths
+# get a list of all instances on a volume
+# get a list of all files under the volumes path
+# compare the two lists
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-fsck - check and repair Nebulous volumes
+
+=head1 SYNOPSIS
+
+    neb-fsck [--db <database>] [--user <username>] [--pass <password>]
+        [--server <server>]
+
+=head1 DESCRIPTION
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --server|-s <URL>
+
+URL of the Nebulous server to connect to.
+
+Optional if the appropriate environment variable is set.
+
+=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.
+
+=back
+
+=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_SERVER>
+
+Equivalent to --server|-s
+
+=item * C<NEB_DB>
+
+Equivalent to --db|-d
+
+=item * C<NEB_USER>
+
+Equivalent to --user|-u
+
+=item * C<NEB_PASS>
+
+Equivalent to --pass|-p 
+
+=back
+
+=head1 CREDITS
+
+Just me, myself, and I.
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=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: /branches/eam_branches/neb-host-20250516/bin/neb-host
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-host	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-host	(revision 42864)
@@ -0,0 +1,283 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.01';
+
+use DBI;
+use Nebulous::Server::SQL;
+use URI::file;
+use SQL::Interp qw( sql_interp );
+use URI;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my ($host, $state, $note,
+    $db, $dbhost, $dbpass, $dbuser,
+    $allocate, $available, $debug,$volume);
+
+# $db     = $ENV{'NEB_DB'} unless $db;
+# $dbhost = $ENV{'NEB_DBHOST'} || 'localhost';
+# $dbuser = $ENV{'NEB_USER'} unless $dbuser;
+# $dbpass = $ENV{'NEB_PASS'} unless $dbpass;
+
+# my $conffile = "/data/ippc64.1/ippitc/ippconfig/nebulous.config";
+# my $conffile = "/home/panstarrs/ipp/ippconfig/nebulous.config";
+my $conffile = "/home/panstarrs/ippps1/ippconfig/nebulous.config";
+
+if (-e $conffile) {
+#    printf STDERR "found config\n";
+    open(IN,$conffile);
+    while(<IN>) {
+	chomp;
+	if ($_ =~ /NEB_DB\s+/) {
+	    $db = (split /\s+/,$_)[2];
+	}
+	elsif ($_ =~ /NEB_DBHOST/) {
+	    $dbhost = (split /\s+/,$_)[2];
+	}
+	elsif ($_ =~ /NEB_USER/) {
+	    $dbuser = (split /\s+/,$_)[2];
+	}
+	elsif ($_ =~ /NEB_PASS/) {
+	    $dbpass = (split /\s+/,$_)[2];
+	}
+    }
+    close(IN);
+#    print STDERR  "$db $dbhost $dbuser $dbpass\n";
+}
+
+GetOptions(
+    'host=s'         => \$host,
+    'state=s'        => \$state,
+    'note=s'         => \$note,
+    'db=s'           => \$db,
+    'dbhost=s'       => \$dbhost,
+    'dbuser=s'       => \$dbuser,
+    'dbpass=s'       => \$dbpass,
+    'volume'         => \$volume,
+    
+    ) || pod2usage( 2 );
+
+if (($#ARGV == 1)&&(!defined($host))&&(!defined($state))) {
+    $host = shift(@ARGV);
+    $state = shift(@ARGV);
+}
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Cannot configure nebulous database", -exitval => 2) unless
+    defined $db && defined $dbhost && defined $dbuser && defined $dbpass;
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$db:host=$dbhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 0,
+    },
+    );
+
+if ((defined($host))&&(defined($state))) {
+    if ($state eq 'up') {
+	$allocate = 1;
+	$available = 1;
+    }
+    elsif ($state eq 'down') {
+	$allocate = 0;
+	$available = 0;
+    }
+    elsif ($state eq 'repair') {
+	$allocate = 0;
+	$available = 1;
+    }
+    else {
+	die "Unknown state '$state'";
+    }
+    
+    my %constraint;
+    if (defined($volume)) {
+	$constraint{'v.name'} = $host;
+    }
+    else {
+	$constraint{'v.host'} = $host;
+    }
+    my %set;
+    $set{'v.allocate'} = $allocate if defined $allocate;
+    $set{'v.available'} = $available if defined $available;
+    $set{'v.note'} = $note if defined $note;
+
+    eval {
+	my ($q, @bind) = sql_interp("UPDATE volume AS v SET", \%set, "WHERE", \%constraint);
+	warn "$q\n" if $debug;
+	my $query = $dbh->prepare($q);
+	$query->execute(@bind);
+	$dbh->commit;
+    };
+    if ($@) {
+	$dbh->rollback;
+	die $@;
+    }
+    eval {
+	my ($q, @bind) = sql_interp("UPDATE mountedvol AS v SET", \%set, "WHERE", \%constraint);
+	warn "$q\n" if $debug;
+	my $query = $dbh->prepare($q);
+	$query->execute(@bind);
+	$dbh->commit;
+    };
+    if ($@) {
+	$dbh->rollback;
+	die $@;
+    }
+}
+
+my $sql = Nebulous::Server::SQL->new();
+my ($q, @bind);
+$q = $sql->get_volumes . " ORDER BY v.host, v.name";
+warn "$q\n" if $debug;
+my $query = $dbh->prepare($q);
+$query->execute(@bind);
+$dbh->commit;
+
+my $format  = "%-15s %-15s %-7s %-8s %-9s %-5s %15s %s\n";
+my @columns = qw(host name mounted allocate available xattr last_modified note);
+printf($format, @columns);
+
+while (my $row = $query->fetchrow_hashref) {
+    unless ((exists($$row{ 'note' })) && (defined($$row{ 'note' }))) {
+	$$row{ 'note' } = '-';
+    }
+
+    printf($format, @$row{@columns});
+}
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-host - simply manage the state of Nebulous storage volumes
+
+=head1 SYNOPSIS
+
+    neb-host [<nebulous host> <nebulous state>]
+    [--note '<status note>']
+    [--host <nebulous host>] [--state <up|down|repair>]
+    [--volume]
+    [--dbhost <database host>] [--db <database name>]
+    [--dbuser <database user>] [--dbpass <database password>]
+
+
+=head1 DESCRIPTION
+
+This program manages a Nebulous server's list of known storage volumes.  It 
+is designed to provide an easy interface to toggle hosts.  If no options are 
+given, the host and state are read directly from the command line.  If nothing
+is specified, prints out the table of currently mounted volumes in nebulous.
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --note <status note>
+
+Set a status note for the specified volume.
+
+=item * --host <nebulous host>
+
+Name of the host on which the nebulous volume(s) reside.
+
+=item * --state <up|down|repair>
+
+Set the state of the nebulous volume:
+    up     Allocate = 1, Available = 1
+    down   Allocate = 0, Available = 0
+    repair Allocate = 0, Available = 1
+
+=item * --volume
+
+Set a particular volume to the given state.  This is a Boolean option,
+and uses the value of --host as the volume name to update.
+
+=item * --db <database name>
+
+Name of database (C<namespace>) to create tables in.
+
+Optional if the appropriate environment variable is set.
+
+=item * --dbhost <database host>
+
+Host name where the database resides.
+
+Defaults to C<localhost>.
+
+=item * --dbuser <database username>
+
+Username to authenticate with.
+
+Optional if the appropriate environment variable is set.
+
+=item * --dbpass <database password>
+
+Password to authenticate with.
+
+Optional if the appropriate environment variable is set.
+
+=back
+
+=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 COPYRIGHT
+
+Copyright (C) 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-voladm>, L<neb-voladd>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/bin/neb-initdb
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-initdb	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-initdb	(revision 42864)
@@ -0,0 +1,148 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2005-2007  Joshua Hoblitt
+#
+# $Id: neb-initdb,v 1.8 2008-06-30 22:44:12 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.03';
+
+use DBI;
+use Nebulous::Server::SQL;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my ($db, $dbhost, $dbuser, $dbpass);
+my ($trap, $actually_do_it);
+# Removed default options using environment variables because it's too dangerous.
+
+GetOptions(
+    'db=s'      => \$db,
+    'host=s'    => \$dbhost,
+    'user=s'    => \$dbuser,
+    'pass=s'    => \$dbpass,
+    'yes_i_know_what_im_doing'  => \$trap,
+    'yes_i_know_what_im_doing_really=s' => \$actually_do_it,
+) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --db --user --pass --host", -exitval => 2 )
+    unless $db && $dbuser && $dbpass && $dbhost;
+pod2usage( -msg => "Aborting due to mistake in options.  Check code.", -exitval => 2) if ($trap);
+pod2usage( -msg => "Aborting due to mistake in options.  Check code.", -exitval => 2) unless (defined($actually_do_it));
+if ($actually_do_it ne 'this_is_intentionally_difficult') {
+    pod2usage( -msg => "Aborting due to mistake in options.  Check code.", -exitval => 2);
+}
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$db:host=$dbhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 1,
+    },
+);
+
+my $sql = Nebulous::Server::SQL->new();
+
+print "Not dropping any existing tables because that's dangerous...";
+#
+#foreach my $statement (@{ $sql->get_db_clear }) {
+#    $dbh->do( $statement );
+#}
+
+print " OK!\nCreating new tables...";
+
+foreach my $statement (@{ $sql->get_db_schema }) {
+    eval {
+	$dbh->do( $statement );
+    };
+    if ($@) {
+	die "Error received: $@\n";
+    }
+}
+
+print " OK\n";
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-initdb - initialize a Nebulous server database
+
+=head1 SYNOPSIS
+
+    neb-initdb [--db <database>] [--user <username>] [--pass <password>] [--host <dbhost>]
+
+=head1 DESCRIPTION
+
+This program initialize a database for use by L<Nebulous::Server> by creating
+the appropriate set of tables.  Any pre-existing tables with conflicting names
+are first removed.  This help file intentionally does not tell you how to make
+the program actually work.
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --db <database>
+
+Name of database (C<namespace>) to create tables in.
+
+
+=item * --user <username>
+
+Username to authenticate with.
+
+
+=item * --pass <password>
+
+Password to authenticate with.
+
+=item * --host <dbhost>
+
+Host to generate the database tables on.
+
+=back
+
+=head1 ENVIRONMENT
+
+No environment variables interact with this program.
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 COPYRIGHT
+
+Copyright (C) 2005-2016  IPP.  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-addvol>, L<neb-df>, L<nebdiskd>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/bin/neb-insedit
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-insedit	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-insedit	(revision 42864)
@@ -0,0 +1,292 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2005-2008  Joshua Hoblitt
+#
+# $Id: neb-insedit,v 1.2 2008-10-01 21:05:57 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.02';
+
+use DBI;
+use Nebulous::Server::SQL;
+use Nebulous::Client;
+use URI::file;
+use URI;
+
+use Digest::MD5;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my ($db, $dbhost, $dbuser, $dbpass, $key, $ins_id, $new_uri, $new_volume,$nebhost);
+
+$db     = $ENV{'NEB_DB'} unless $db;
+$dbhost = $ENV{'NEB_DBHOST'} || 'localhost';
+$dbuser = $ENV{'NEB_USER'} unless $dbuser;
+$dbpass = $ENV{'NEB_PASS'} unless $dbpass;
+$nebhost= $ENV{'NEB_SERVER'} unless $nebhost;
+
+GetOptions(
+    'db|d=s'            => \$db,
+    'host=s'            => \$dbhost,
+    'pass|p=s'          => \$dbpass,
+    'user|u=s'          => \$dbuser,
+    'neb_host=s'        => \$nebhost,
+    'key=s'             => \$key,
+    'ins_id=s'          => \$ins_id,
+    'uri=s'             => \$new_uri,
+    'volume=s'          => \$new_volume,
+) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --db --user --pass --key --ins_id --uri --volume", -exitval => 2 )
+    unless $db && $dbuser && $dbpass && $ins_id && $new_uri && $key && $new_volume;
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$db:host=$dbhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 1,
+    },
+);
+
+# Connect and retrieve instance information via stat:
+my $neb = Nebulous::Client->new(
+    proxy => "$nebhost",
+    );
+
+die "can't connected to Nebulous Server: $nebhost"
+    unless defined $neb;
+
+
+# Pre-emptively attempt repair of file.  This will ~double our execution time, but should help the efficiency.
+system("neb-repair $key");
+if ($? >> 8 != 0) {
+    die "Failure to repair $key.\n";
+}
+
+# Do checks now.
+
+my $stat = $neb->stat($key);
+die "nebulous key: $key not found" unless $stat;   
+my $instances = $neb->find_instances($key, 'any');
+die "no instances found" unless $instances;   
+
+my $user_copies;
+# eval {
+#     $user_copies = $neb->getxattr($key, "user.copies");
+# };
+$user_copies = 1;
+unless(defined($user_copies)) {
+    $user_copies = 1;
+}
+my @validation;
+my %md5sum_uniq;
+my $existing_copies = 0;
+my @existance;
+my @md5sums;
+my @files = map {URI->new($_)->file if $_} @$instances;
+
+for (my $i = 0; $i <= $#files; $i++) {
+    if (-e $files[$i]) {
+	$existance[$i] = 1;
+	$existing_copies++;
+	my $fh;
+	open($fh,$files[$i]);
+	$md5sums[$i] = Digest::MD5->new->addfile($fh)->hexdigest;
+	$md5sum_uniq{$md5sums[$i]} = 1;
+	close($fh);
+    }
+    else {
+	$existance[$i] = 0;
+	$md5sums[$i] = 'NON-EXISTANT';
+	$md5sum_uniq{$md5sums[$i]} = 1;
+    }
+    $validation[$i] = sprintf("% 3d %32s %s",
+			      $existance[$i],
+			      $md5sums[$i],
+			      $files[$i]);
+}
+# Confirm everything is in a valid state
+my $N_md5sums = scalar(keys %md5sum_uniq);
+if ($N_md5sums != 1) {
+    die "Cannot modify this instance! storage_object $key in inconsistent state!";
+}
+if ($#files < 1) {
+    die "Insufficient instances! $key!";
+}
+my $test_md5sum = $md5sums[0];
+
+# Copy instance to new uri
+my $destination_file = URI->new($new_uri)->file;
+my $source_file      = $files[0];
+system("cp $source_file $destination_file");
+system("glockfile $destination_file xcld 0");
+
+my $fh;
+open($fh,$destination_file);
+my $new_md5sum = Digest::MD5->new->addfile($fh)->hexdigest;
+close($fh);
+
+if ($new_md5sum ne $test_md5sum) {
+    die "md5sum mismatch with new copy. $destination_file ($new_md5sum) != $source_file ($test_md5sum) Exiting!";
+}
+
+# Update database table
+my $sql = Nebulous::Server::SQL->new();
+
+my $query = $dbh->prepare( $sql->get_volume_by_name );
+$query->execute( $new_volume );
+my ($vol_id, $vol_name, $vol_host, $vol_path) = $query->fetchrow_array;
+$query->finish;
+
+print "$new_volume $vol_id $vol_name $vol_host $vol_path $new_uri $ins_id\n";
+unless (defined($vol_id)) {
+    die "Invalid destination volume.";
+}
+
+$query = $dbh->prepare( $sql->update_instance_uri );
+$query->execute( $vol_id, $new_uri, $ins_id );
+$query->finish;
+
+unlink($source_file);
+print " OK\n";
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-insedit - edit the instance information
+
+=head1 SYNOPSIS
+
+    neb-insedit --key <neb_key> --ins_id <ins_id> --uri <destination_uri> --volume <destination_volume>
+    [--db <database>] [--user <username>] [--pass <password>] [--host <hostname]
+
+=head1 DESCRIPTION
+
+This program allows the given instance of an object to be moved from one volume/uri to another.
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --key <neb_key>
+
+Nebulous key for the storage object associated with the instance to move.
+
+=item * --ins_id <ins_id>
+
+Instance identifier that will be moved
+
+=item * --uri <destination_uri>
+
+Final location desired for this instance.  Should be of the form "file://data/ipp0XX.0/nebulous/aa/aa/ins_id.nebulous:directory:tree:file".
+
+=item * --volume <destination volume>
+
+Volume name that this instance will be moved to.
+
+=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.
+
+=item * --neb_host <nebulous client server>
+
+Location of the client side server to connect to.
+
+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 
+
+=item * C<NEB_SERVER>
+
+Equivalent to --neb_host
+
+=back
+
+=head1 CREDITS
+
+Josh Hoblitt and Chris Waters 
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=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: /branches/eam_branches/neb-host-20250516/bin/neb-voladd
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-voladd	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-voladd	(revision 42864)
@@ -0,0 +1,213 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2005-2008  Joshua Hoblitt
+#
+# $Id: neb-voladd,v 1.2 2008-10-01 21:05:57 jhoblitt Exp $
+
+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, $mountpoint, $vname, $vhost, $uri, $note);
+
+$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,
+    'mountpoint|m=s'    => \$mountpoint,
+    'pass|p=s'          => \$dbpass,
+    'uri|u=s'           => \$uri,
+    'user|u=s'          => \$dbuser,
+    'vhost=s'           => \$vhost,
+    'vname|n=s'         => \$vname,
+    'note=s'            => \$note,
+) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --db --user --pass --mountpoint --vname --vhost --uri", -exitval => 2 )
+    unless $db && $dbuser && $dbpass && $mountpoint && $vname && $vhost && $uri;
+
+my $dbh = DBI->connect(
+    # if you want to work with the localhost database but get socket path errors,
+    # you should set the server host as 127.0.0.1 (not localhost)
+
+    # the following tells DBI to read the mysql/my.cnf files
+    #  "DBI:mysql:database=$db:host=$dbhost:mysql_read_default_group=dbd_mysql",
+
+    "DBI:mysql:database=$db:host=$dbhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 1,
+    },
+);
+
+my $sql = Nebulous::Server::SQL->new();
+
+print "Checking URI...";
+
+my $path = URI->new($uri)->path;
+unless (-d $path) {
+    die "path: $path dirived from URI: $uri does not exist";
+}
+unless (-d $mountpoint) {
+    die "path: $mountpoint does not exist";
+}
+
+print " OK\n";
+
+print "Adding volume...";
+
+my $query = $dbh->prepare( $sql->new_volume );
+$query->execute( $vname, $vhost, $path, $mountpoint, $note);
+
+print " OK\n";
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-voladd - add a storage volume to a Nebulous server
+
+=head1 SYNOPSIS
+
+    neb-voladd --vname <volume name> --vhost <host name> --uri <volume uri>
+    [--db <database>] [--user <username>] [--pass <password>] [--host <hostname]
+
+=head1 DESCRIPTION
+
+This program initialize a database for use by L<Nebulous::Server> by creating
+the appropriate set of tables.  Any pre-existing tables with conflicting names
+are first removed.
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --vname <volume name>
+
+Symbolic name of volume being added.
+
+=item * --vhost <host name>
+
+Name of the host that containes the C<URI> of the volume being added. 
+
+=item * --uri|-u <volume uri>
+
+URI to the volume being added.
+
+=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>.
+
+=item * --note <note>
+
+Add a note for this volume.
+
+=item * --mountpoint <path>
+
+Path to the mountpoint of the filesystem on which C<--uri> resides.
+
+=back
+
+=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
+
+Just me, myself, and I.
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=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: /branches/eam_branches/neb-host-20250516/bin/neb-voladm
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/neb-voladm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/neb-voladm	(revision 42864)
@@ -0,0 +1,320 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2008  Joshua Hoblitt
+#
+# $Id: neb-voladm,v 1.12 2008-10-16 22:29:03 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.01';
+
+use DBI;
+use Nebulous::Server::SQL;
+use URI::file;
+use SQL::Interp qw( sql_interp );
+use URI;
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my (
+    $allocate,
+    $available,
+    $mounted,
+    $db,
+    $dbhost,
+    $dbpass,
+    $dbuser,
+    $debug,
+    $vhost,
+    $vname,
+    $xattr,
+    $cab_id,
+    $note,
+);
+
+$db     = $ENV{'NEB_DB'} unless $db;
+$dbhost = $ENV{'NEB_DBHOST'} || 'localhost';
+$dbuser = $ENV{'NEB_USER'} unless $dbuser;
+$dbpass = $ENV{'NEB_PASS'} unless $dbpass;
+
+GetOptions(
+    'allocate=i'        => \$allocate,
+    'available=i'       => \$available,
+    'mounted'           => \$mounted,
+    'db|d=s'            => \$db,
+    'debug'             => \$debug,
+    'host=s'            => \$dbhost,
+    'pass|p=s'          => \$dbpass,
+    'user|u=s'          => \$dbuser,
+    'vhost=s'           => \$vhost,
+    'vname|n=s'         => \$vname,
+    'cab_id|c=i'        => \$cab_id,
+    'xattr=i'           => \$xattr,
+    'note=s'            => \$note,
+) || pod2usage( 2 );
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --db --user --pass", -exitval => 2 )
+    unless defined $db && defined $dbuser && defined $dbpass;
+
+# are we listing the volumes or updating?
+if (defined $allocate or defined $available or defined $xattr or defined $cab_id) {
+    pod2usage( -msg => "Options: --allocate, --available, --xattr, and --cab_id Require options --vhost or --vname", -exitval => 2 )
+        unless defined $vhost or defined $vname;
+    pod2usage( -msg => "Options: --allocate, --available, and --xattr Must be 0 or 1", -exitval => 2)
+        if (defined $allocate and $allocate !~ m/^[01]$/)
+        or (defined $available and $available !~ m/^[01]$/)
+        or (defined $xattr and $xattr !~ m/^[01235]$/);
+}
+
+my $dbh = DBI->connect(
+    "DBI:mysql:database=$db:host=$dbhost",
+    $dbuser,
+    $dbpass,
+    {
+        RaiseError => 1,
+        PrintError => 0,
+        AutoCommit => 0,
+    },
+);
+
+my %constraint;
+$constraint{'v.host'} = $vhost if defined $vhost;
+$constraint{'v.name'} = $vname if defined $vname;
+
+my %set;
+$set{'v.allocate'} = $allocate if defined $allocate;
+$set{'v.available'} = $available if defined $available;
+$set{'v.xattr'} = $xattr if defined $xattr;
+$set{'v.cab_id'} = $cab_id if defined $cab_id;
+$set{'v.note'} = $note if defined $note;
+
+if (%set) {
+    eval {
+        my ($q, @bind) = sql_interp("UPDATE volume AS v SET", \%set, "WHERE", \%constraint);
+        warn "$q\n" if $debug;
+        my $query = $dbh->prepare($q);
+        $query->execute(@bind);
+        $dbh->commit;
+    };
+    if ($@) {
+        $dbh->rollback;
+        die $@;
+    }
+    if ($mounted) {
+	eval {
+	    my ($q, @bind) = sql_interp("UPDATE mountedvol AS v SET", \%set, "WHERE", \%constraint);
+	    warn "$q\n" if $debug;
+	    my $query = $dbh->prepare($q);
+	    $query->execute(@bind);
+	    $dbh->commit;
+	};
+	if ($@) {
+	    $dbh->rollback;
+	    die $@;
+	}
+    }
+} 
+
+my $sql = Nebulous::Server::SQL->new();
+my ($q, @bind);
+if (%constraint) {
+    ($q, @bind) = sql_interp($sql->get_volumes . "WHERE", \%constraint, "ORDER BY v.name");
+} else {
+    $q = $sql->get_volumes . " ORDER BY v.host, v.name";
+}
+
+warn "$q\n" if $debug;
+my $query = $dbh->prepare($q);
+$query->execute(@bind);
+$dbh->commit;
+
+my $format  = "%-15s %-15s %-10s %-10s %-10s %-10s\n";
+my @columns = qw(host name mounted allocate available xattr);
+printf($format, @columns);
+
+while (my $row = $query->fetchrow_hashref) {
+    printf($format, @$row{@columns});
+}
+
+__END__
+
+=pod
+
+=head1 NAME
+
+neb-voladm - manage Nebulous storage volumes
+
+=head1 SYNOPSIS
+
+    neb-voladm [--vname <volume name>] [--vhost <volume host>]
+    [--allocate <0|1>] [--available <0|1>] [--xattr <0|1>]
+    [--cab_id <cab_id>] [--note <note>]
+    [--host <database host> ] [--db <database name>]
+    [--user <database username>] [--pass <database password>]
+
+=head1 DESCRIPTION
+
+This program manages a Nebulous server's list of known storage volumes.  It is
+capable of listing all storage volumes and modifying a volume's attribute
+flags.
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --vname <volume name>
+
+Symbolic name of the storage volume.
+
+Optional.
+
+=item * --vhost <volume host>
+
+Name of the host on which the storage volume(s) reside.
+
+Optional.
+
+=item * --allocate <0|1>
+
+=item * --available <0|1>
+
+=item * --xattr <0|1>
+
+Enables/Disables these flags on the storage volume.
+
+Requires either the C<--vname> or c<--vhost> options.
+
+=item * --cab_id <cab_id>
+
+Sets the cabinet id for this storage volume.
+
+Requires either the C<--vname> or c<--vhost> options.
+
+=item * --note <note>
+
+Set a note for this volume.
+
+Requires either the C<--vname> or c<--vhost> options.
+
+=item * --db|-d <database name>
+
+Name of database (C<namespace>) to create tables in.
+
+Optional if the appropriate environment variable is set.
+
+=item * --host <database host>
+
+Host name where the database resides.
+
+Defaults to C<localhost>.
+
+=item * --user|-u <database username>
+
+Username to authenticate with.
+
+Optional if the appropriate environment variable is set.
+
+=item * --pass|-p <database password>
+
+Password to authenticate with.
+
+Optional if the appropriate environment variable is set.
+
+=back
+
+=head1 EXAMPLE USAGE
+
+=head2 Listing all volumes
+
+    $ neb-voladm
+    host            name            mounted    allocate   available  xattr     
+    ipp004          ipp004.0        1          0          1          0         
+    ipp005          ipp005.0        1          0          1          0         
+    ipp006          ipp006.0        1          1          1          0
+    ...
+
+=head2 Listing just the volumes on host C<ipp025>
+
+    $ neb-voladm --vhost ipp025
+    host            name            mounted    allocate   available  xattr     
+    ipp025          ipp025.0        1          1          0          0     
+
+=head2 Listing just the volume symbolically named C<ipp006.0>
+
+    $ neb-voladm -vname ipp006.0
+    host            name            mounted    allocate   available  xattr     
+    ipp006          ipp006.0        1          1          1          0    
+
+=head2 Turning the C<available> flag off for all volumes on the host named C<ipp012>
+
+    $ neb-voladm --vhost ipp012 --available 0
+    host            name            mounted    allocate   available  xattr     
+    ipp012          ipp012.0        0          1          0          
+
+=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
+
+Just me, myself, and I.
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=head1 COPYRIGHT
+
+Copyright (C) 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-voladd>, L<nebdiskd>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/bin/nebdiskd
===================================================================
--- /branches/eam_branches/neb-host-20250516/bin/nebdiskd	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/bin/nebdiskd	(revision 42864)
@@ -0,0 +1,586 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2007-2009  Joshua Hoblitt
+
+use strict;
+use warnings FATAL => qw( all );
+
+use vars qw( $VERSION );
+$VERSION = '0.03';
+
+use Config::YAML;
+use DBI;
+use File::Spec;
+use Log::Log4perl;
+use Nebulous::Server::SQL;
+use Net::Server::Daemonize qw( daemonize unlink_pid_file );
+
+use Getopt::Long qw( GetOptions :config auto_help auto_version );
+use Pod::Usage qw( pod2usage );
+
+my (
+    $debug,
+    $db,
+    $dbhost,
+    $dbuser,
+    $dbpass,
+    $pidfile,
+    $stop,
+    $restart,
+    $verbose,
+    $user,
+    $group,
+    $retry,
+);
+
+GetOptions(
+    'dbhost|H=s'    => \$dbhost,
+    'dbpass|p=s'    => \$dbpass,
+    'dbuser|u=s'    => \$dbuser,
+    'db|D=s'        => \$db,
+    'debug|d'       => \$debug,
+    'group|g=s'     => \$group,
+    'pidfile=s'     => \$pidfile,
+    'restart|r'     => \$restart,
+    'retry=i'       => \$retry,
+    'stop|s'        => \$stop,
+    'user|U=s'      => \$user,
+    'verbose|v'     => \$verbose,
+) || pod2usage( 2 );
+
+# set the process name for easy pgrep-ability
+$0 = 'nebdiskd';
+#my $rcfile = "$ENV{HOME}/.nebdiskrc";
+my $rcfile = $ENV{NEBDISKDRC};
+unless (defined($rcfile)) {
+    $rcfile = '/etc/nebdiskd.rc';
+}
+$rcfile = File::Spec->canonpath($rcfile);
+
+my $c = read_rcfile($rcfile);
+
+$db         ||= $c->get_db      || $ENV{'NEB_DB'};
+$dbhost     ||= $c->get_dbhost  || $ENV{'NEB_DBHOST'} || 'localhost';
+$dbuser     ||= $c->get_dbuser  || $ENV{'NEB_USER'};
+$dbpass     ||= $c->get_dbpass  || $ENV{'NEB_PASS'} || undef;
+$pidfile    ||= $c->get_pidfile || "/var/tmp/nebdiskd";
+$user       ||= $c->get_user    || $<; # user
+$group      ||= $c->get_group   || $); # group
+$retry      ||= $c->get_retry   || 1;
+
+# stuff to controll failures.
+my %host_failure_counts = ();
+my %host_removed = ();
+my $failure_limit = 5;
+
+#my $mounts = $c->get_mounts;
+my $poll_interval = $c->get_poll_interval || 5;
+
+if ($restart || $stop) {
+    my $status = kill_pid_file($pidfile);
+    exit($status) if $stop;
+}
+
+$c->set_db($db);
+$c->set_dbhost($dbhost);
+$c->set_dbuser($dbuser);
+$c->set_dbpass($dbpass);
+$c->set_pidfile($pidfile);
+$c->set_poll_interval($poll_interval);
+$c->set_retry($retry);
+$c->write;
+
+pod2usage( -msg => "Unknown option: @ARGV", -exitval => 2 ) if @ARGV;
+pod2usage( -msg => "Required options: --db --dbuser", -exitval => 2 )
+    unless $db && $dbuser;
+
+# start up logging
+my $conf = '
+    log4perl.category.nebdiskd = WARN, Screen, SERVERLOGFILE
+
+    log4perl.appender.Screen        = Log::Log4perl::Appender::Screen
+    log4perl.appender.Screen.stderr = 1
+    log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
+    log4perl.appender.Screen.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} | %H | %p | %M - %m%n
+
+    log4perl.appender.SERVERLOGFILE           = Log::Log4perl::Appender::File
+    log4perl.appender.SERVERLOGFILE.filename  = /tmp/nebdiskd.log
+    log4perl.appender.SERVERLOGFILE.mode      = append
+    log4perl.appender.SERVERLOGFILE.layout    = Log::Log4perl::Layout::PatternLayout
+    log4perl.appender.SERVERLOGFILE.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} | %H | %p | %M - %m%n
+    log4perl.appender.SERVERLOGFILE.size      = 10485760
+
+    log4perl.filter.MatchWarn  = Log::Log4perl::Filter::LevelMatch
+    log4perl.filter.MatchWarn.LevelToMatch  = WARN
+    log4perl.filter.MatchWarn.AcceptOnMatch = off
+
+    log4perl.appender.Limiter              = Log::Log4perl::Appender::Limit
+    log4perl.appender.Limiter.appender     = Mailer
+    log4perl.appender.Limiter.block_period = 300
+';
+# I've removed this bit.
+#     log4perl.appender.Mailer         = Log::Dispatch::Email::MailSend
+#     log4perl.appender.Mailer.to      = ps-ipp-ops@ifa.hawaii.edu
+#     log4perl.appender.Mailer.subject = nebdiskd alert
+#     log4perl.appender.Mailer.buffered = 0
+#     log4perl.appender.Mailer.Filter= MatchWarn
+#     log4perl.appender.Mailer.layout = Log::Log4perl::Layout::PatternLayout
+#     log4perl.appender.Mailer.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} | %H | %p | %M - %m%n
+
+
+Log::Log4perl::init(\$conf);
+my $log = Log::Log4perl::get_logger("nebdiskd");
+$log->level('WARN');
+$log->level('DEBUG') if $debug;
+
+daemonize(
+    $user,     # User
+    $group,    # Group,
+    $pidfile,  # PID file
+) unless $debug;
+
+$SIG{TERM} = sub { unlink_pid_file($pidfile); exit(); };
+$SIG{INT}  = $SIG{TERM};
+$SIG{HUP}  = sub { $c = read_rcfile($rcfile) };
+
+my $date = localtime();
+$log->warn("BEGIN: $date with RCFILE: $rcfile");
+
+while (1) {
+    eval {
+        poll_mounts(
+                db              => $db,
+                dbhost          => $dbhost,
+                dbuser          => $dbuser,
+                dbpass          => $dbpass,
+                poll_interval   => $poll_interval,
+                debug           => $debug,
+                retry           => $retry,
+        );
+    };
+    if ($@) {
+        $log->warn($@);
+    }
+
+    sleep $poll_interval;
+}
+
+$log->logdie("poll loop exited -- THIS SHOULD NOT HAPPEN");
+
+
+sub poll_mounts
+{
+    my %p = @_;
+
+    my $db              = $p{db} or return;
+    my $dbhost          = $p{dbhost} or return;
+    my $dbuser          = $p{dbuser} or return;
+#    my $dbpass          = $p{dbpass} or return;
+    my $poll_interval   = $p{poll_interval} || 60;
+    my $debug           = $p{debug} || 0;
+    my $retry           = $p{retry} || 1;
+
+    # setup the db on every pass incase the database died on us
+    my $dbh = setup_db(
+        db      => $db,
+        dbhost  => $dbhost,
+        dbuser  => $dbuser,
+        dbpass  => $dbpass,
+    );
+
+    eval {
+        my $r_query = $dbh->prepare_cached("REPLACE INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, ?, ?,note FROM volume WHERE mountpoint = ?");
+        my $d_query = $dbh->prepare_cached("DELETE FROM mountedvol WHERE mountpoint = ?");
+
+        # get list of mount points
+        my $mounts = [];
+        {
+            # there may be multiple vol_ids per mountpoint but we only need to
+            # check each mountpoint once
+            my $query = $dbh->prepare_cached("SELECT DISTINCT host, mountpoint FROM volume WHERE xattr != 5");
+            $query->execute;
+            while (my $row = $query->fetchrow_hashref) {
+                push @$mounts, $row;
+            }
+            $query->finish;
+        }
+
+        # determine valid mountpoints
+        foreach my $mnt (@$mounts) {
+            my $mountpoint = $mnt->{'mountpoint'};
+            my $alt_mountpoint = $mountpoint;
+            $alt_mountpoint =~ s/data/export/;
+            my $hostname = $mnt->{'host'};
+            my $valid_mountpoint = 1;
+
+            # fetch stats on the device using ssh and df
+            my $dev_info = df_through_ssh($hostname, $alt_mountpoint, 1024);
+            unless (defined $dev_info) {
+                $valid_mountpoint = 0;
+                $log->error("can't find device info for $mountpoint / $alt_mountpoint");
+                $d_query->execute($mountpoint);
+                next;
+            }
+
+            # find vol_id(s) for mountpoint
+            $r_query->execute(@$dev_info{qw( blocks used )}, $mountpoint);
+            $log->debug("adding $mountpoint to db");
+
+        }
+    };
+    if ($@) {
+        $log->logdie($@);
+    }
+}
+
+sub read_rcfile
+{
+    my $rcfile = shift;
+
+    return unless defined $rcfile;
+
+    if (!-f $rcfile) {
+        open(my $fh, '>', $rcfile) or $log->logdie("can't open file: $!");
+        close($fh) or $log->logdie("can't close file:$!");
+    }
+
+    return Config::YAML->new(
+            config => $rcfile,
+            output => $rcfile,
+    );
+}
+
+
+sub setup_db
+{
+    my %p = @_;
+
+    # $p{dbpass} may be undef;
+
+    return unless defined $p{db}
+              and defined $p{dbhost}
+              and defined $p{dbuser};
+
+    my $sql = Nebulous::Server::SQL->new;
+
+    my $dbh = DBI->connect_cached(
+        "DBI:mysql:database=$p{db}:host=$p{dbhost}",
+        $p{dbuser},
+        $p{dbpass},
+        {
+            RaiseError => 1,
+            PrintError => 1,
+            AutoCommit => 1, # don't want this to be trasnactional
+        },
+    );
+
+    eval {
+        $dbh->do( $sql->set_transaction_model );
+#        $dbh->commit;
+    };
+    if ($@) {
+#        $dbh->rollback;
+        $log->logdie($@);
+    }
+
+    return $dbh;
+}
+
+
+sub stat_dirs
+{
+    my $mounts = shift;
+
+    return unless defined $mounts;
+
+    foreach my $path (@$mounts) {
+        if (stat File::Spec->canonpath($path)) {
+            $log->debug("stated $path");
+        } else {
+            $log->warn("can not stat path: $path");
+        }
+    }
+}
+
+#
+# This function, originally named check_pid_file() was copied from
+# Net::Server::Daemonize '0.05' licensed under the Perl license.  It has been
+# renamed and modified to kill off process who's PID value is stored in pidfile.
+#
+
+sub kill_pid_file {
+  my $pid_file = shift;
+
+  ### no pid_file = return success
+  return 1 unless -e $pid_file;
+
+  ### get the currently listed pid
+  if( ! open(_PID,$pid_file) ){
+    $log->logdie("Couldn't open existant pid_file \"$pid_file\" [$!]");
+  }
+  my $_current_pid = <_PID>;
+  close _PID;
+  my $current_pid = $_current_pid =~ /^(\d{1,10})/ ? $1 : $log->logdie("Couldn't find pid in existing pid_file");
+
+  my $exists = undef;
+
+  ### try a proc file system
+  if( -d '/proc' ) {
+    $exists = -e "/proc/$current_pid";
+
+  ### try ps
+  #}elsif( -x '/bin/ps' ){ # not as portable
+  # the ps command itself really isn't portable
+  # this follows BSD syntax ps (BSD's and linux)
+  # this will fail on Unix98 syntax ps (Solaris, etc)
+  } elsif( `ps h o pid p $$` =~ /^\s*$$\s*$/ ){ # can I play ps on myself ?
+      $exists = `ps h o pid p $current_pid`;
+  }
+
+  ### running process exists, ouch
+    if( $exists ){
+
+        if( $current_pid == $$ ){
+            $log->warn("Pid_file created by this same process. Doing nothing.");
+            return 1;
+        }else{
+            kill 'TERM', $current_pid
+                or $log->logdie("Failed to signal process ($current_pid)");
+            unlink $pid_file || $log->logdie("Couldn't remove pid_file \"$pid_file\" [$!]");
+        }
+    }
+
+    return 0;
+}
+
+sub df_through_ssh {
+    my $hostname = shift;
+    my $mountpoint = shift;
+    my $units = shift;
+    $log->debug("Getting info from mountpoint = [$mountpoint] / unit = [$units] / hostname = [$hostname]");
+    my $df_data = qx/ssh $hostname 'df $mountpoint' | grep -v Filesystem/;
+    $df_data =~ s/\s+/:/g;
+    $log->debug("Received from ssh = [$df_data]");
+    my %df_info = ();
+    $df_info{"blocks"} = 0;
+    $df_info{"used"} = 0;
+    unless ($df_data) {
+        # We cannot connect to the host for some reason However, the
+        # partition can be mounted on some other hosts (in the case of
+        # an original host failure or upgrade for instance) Let's look
+        # for an entry in /etc/autofs/auto.data starting with
+        # <hostname>.[012] If it's in there, return (blocks => 1, used
+        # => 1). The partition is mounted but not available for
+        # replication (since blocks-used = 0 and used/blocks=1=100%).
+        my $partition_name = $mountpoint;
+        $partition_name =~ s/\/export\///;
+        $log->debug("Looking for $partition_name in /etc/autofs/auto.data");
+        open(AUTODATA, "</etc/autofs/auto.data") or die("Can't open /etc/autofs/auto.data");
+        foreach my $line (<AUTODATA>) {
+            if ($line =~ /^$partition_name/) {
+                $log->debug("$partition_name found in /etc/autofs/auto.data");
+                close(AUTODATA);
+                $df_info{"blocks"} = 1;
+                $df_info{"used"} = 1;
+                return \%df_info;
+            }
+        }
+        $log->debug("$partition_name NOT found in /etc/autofs/auto.data");
+        close(AUTODATA);
+        return undef;
+    }
+    my @data = split(/:/, $df_data);
+    $df_info{"blocks"} = $data[1];
+    $df_info{"used"} = $data[1]-$data[3];
+    $log->debug("blocks = " . $df_info{"blocks"} . " / used = " . $df_info{"used"});
+    return \%df_info;
+}
+
+__END__
+
+=pod
+
+=head1 NAME
+
+nebdiskd - Nebulous mounted volume capacity monitoring daemon
+
+=head1 SYNOPSIS
+
+    nebdiskd [--db|-D <db name>] [--dbhost|-H <db hostname>
+    [--dbuser|-u <db username>] [--dbpass|-p <db password>] [--debug|-d]
+    [--user|-U <username>] [--group|-g <groupname>] [--pidfile <filename>]
+    [--retry <n>] [--stop|-s] [--restart|-r] [--verbose|-v]
+
+=head1 DESCRIPTION
+
+This program looks for mounted volumes, at a configurable interval, and records
+statistics about them into a database.
+
+=head1 OPTIONS
+
+=over 4
+
+=item * --db|-D <db name>
+
+Name of database (C<namespace>) to write too.
+
+Optional if defined in the F<.nebdiskdrc> file or the appropriate environment
+variable is set.
+
+=item * --dbuser|-u <username>
+
+Username to authenticate with.
+
+Optional if defined in the F<.nebdiskdrc> file or the appropriate environment
+variable is set.
+
+=item * --dbpass|-p <password>
+
+Password to authenticate with.
+
+Optional if defined in the F<.nebdiskdrc> file or the appropriate environment
+variable is set or if the database does not require a password.
+
+=item * --dbhost|-H <hostname>
+
+Database host to connect to.
+
+Optional if defined in the F<.nebdiskdrc> file or the appropriate environment
+variable is set.
+
+=item * --debug|-d
+
+This flag prevents the program from "daemonizing" so output can be sent to
+C<stdout>/C<stderr> and a debugger used on the running process.
+
+=item * --pidfile <filename>
+
+Filename to log the running daemon's PID too.
+
+Optional, if this option is omitted and no value is defined in the
+F<.nebdiskrc> then no pidfile is created.
+
+=item * --stop|-s
+
+Uses the pidfile to kill an already running daemon.
+
+=item * --restart|-r
+
+Uses the pidfile to kill an already running daemon and then starts a new
+instance.
+
+=item * --retry
+
+The number of times to test a mountpoint for "mountedness" before giving up on
+it.
+
+=item * --verbose|-v
+
+Turns on informational/debugging messages to be sent to the
+C<stderr>/C<stdout>.  This option is probably not very usefully unless combined
+with C<--debug>.
+
+=item * --user|-U
+
+user account to run daemon as.
+
+=item * --group|-g
+
+group to run daemon as.
+
+=back
+
+=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_USER>
+
+Equivalent to --dbuser|-u
+
+=item * C<NEB_PASS>
+
+Equivalent to --dbpass|-p
+
+=back
+
+=head1 RCFILE
+
+This program will attempt to read an C<rcfile> from F<$HOME/.nebdiskrc> and if
+found will use it's values to override program defaults.  Please not that the
+precedence for values is: command line, rcfile, environment variables.
+
+The format of the C<rcfile> is in L<YAML> format and uses the following values:
+
+    ---
+    db: nebulous
+    dbpass: '@neb@'
+    dbuser: nebulous
+    pidfile: /var/tmp/nebdiskd
+    poll_interval: 5
+    retry: 3
+
+The values C<db>, C<dbpass>, C<dbuser>, and C<pidfile> have the same semantics
+as their command line and/or environment variable equivalents.
+
+=over 4
+
+This value may be omitted or left blank.
+
+=item * C<poll_interval>
+
+The number of seconds to wait between checks for mounted volumes.
+
+This value may be omitted or left blank.  The default value is C<60>s.
+
+=item * C<retry>
+
+Same as C<--retry>.
+
+=back
+
+=head1 CREDITS
+
+Just me, myself, and I.
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2007-2009  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<YAML>, L<neb-addvol>, L<neb-df>, L<neb-initdb>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/docs/apache2-mod_perl-startup.pl
===================================================================
--- /branches/eam_branches/neb-host-20250516/docs/apache2-mod_perl-startup.pl	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/docs/apache2-mod_perl-startup.pl	(revision 42864)
@@ -0,0 +1,51 @@
+use lib qw(/home/httpd/perl);
+
+# enable if the mod_perl 1.0 compatibility is needed
+#use Apache2::compat ();
+
+use ModPerl::Util (); #for CORE::GLOBAL::exit
+
+use Apache2::RequestRec ();
+use Apache2::RequestIO ();
+use Apache2::RequestUtil ();
+
+use Apache2::ServerRec ();
+use Apache2::ServerUtil ();
+use Apache2::Connection ();
+use Apache2::Log ();
+
+use APR::Table ();
+
+use ModPerl::Registry ();
+
+use Apache2::Const -compile => ':common';
+use APR::Const -compile => ':common';
+
+use Apache::DBI;
+use DBI;
+use Nebulous::Server::SOAP;
+use Nebulous::Server::Apache;
+use Nebulous::Server;
+
+my $dsn         = 'DBI:mysql:database=nebulous:host=nebulous.ipp.ifa.hawaii.edu';
+my $dbuser      = 'nebulous';
+my $dbpasswd    = 'XXXX';
+
+#$Apache::DBI::DEBUG = 1;
+#Apache::DBI->connect_on_init( $dsn, $dbuser, $dbpasswd );
+Apache::DBI->setPingTimeOut($dsn, 10);
+
+my $config = Nebulous::Server::Config->new(
+    trace       => 'warn',
+					   );
+$config->add_db(
+    dbindex     => 0,
+    dsn         => $dsn,
+    dbuser      => $dbuser,
+    dbpasswd    => $dbpasswd,
+		);
+
+Nebulous::Server::SOAP->new_on_init($config);
+
+
+1;
Index: /branches/eam_branches/neb-host-20250516/docs/changes.txt
===================================================================
--- /branches/eam_branches/neb-host-20250516/docs/changes.txt	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/docs/changes.txt	(revision 42864)
@@ -0,0 +1,5 @@
+-- 2010-06-24 edit to add notes.
+
+ALTER TABLE volume ADD COLUMN note VARCHAR(255) AFTER cab_id;
+ALTER TABLE mountedvol ADD COLUMN note VARCHAR(255) AFTER used;
+
Index: /branches/eam_branches/neb-host-20250516/docs/database_setup.txt
===================================================================
--- /branches/eam_branches/neb-host-20250516/docs/database_setup.txt	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/docs/database_setup.txt	(revision 42864)
@@ -0,0 +1,59 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id: database_setup.txt,v 1.9 2008-06-20 03:34:28 jhoblitt Exp $
+
+### Database Setup
+
+### MySQL 4.1
+
+DROP DATABASE IF EXISTS nebulous;
+CREATE DATABASE nebulous;
+GRANT ALL PRIVILEGES ON nebulous.* TO 'nebulous'@'ipp019'
+    IDENTIFIED BY '@neb@' WITH GRANT OPTION;
+FLUSH PRIVILEGES;
+USE nebulous;
+
+### Initialization
+
+# localhost
+
+$ mkdir /tmp/po01 /tmp/po02
+INSERT INTO volume VALUES (1,'po01','file:/tmp/po01');
+INSERT INTO volume VALUES (1,'po02','file:/tmp/po02');
+
+# NFS
+
+INSERT INTO volume VALUES (1,'po01','file:/netdisks/po01');
+INSERT INTO volume VALUES (2,'po02','file:/netdisks/po02');
+INSERT INTO volume VALUES (3,'po03','file:/netdisks/po03');
+INSERT INTO volume VALUES (4,'po04','file:/netdisks/po04');
+INSERT INTO volume VALUES (5,'po05','file:/netdisks/po05');
+INSERT INTO volume VALUES (6,'po06','file:/netdisks/po06');
+INSERT INTO volume VALUES (7,'po07','file:/netdisks/po07');
+INSERT INTO volume VALUES (8,'po08','file:/netdisks/po08');
+
+INSERT INTO class VALUES (1,3,0,'raw image data');
+
+### Example queries
+
+LOAD DATA INFILE '/home/moa/jhoblitt/tmp/storage_objects.txt' INTO TABLE storage_object; 
+LOAD DATA INFILE '/home/moa/jhoblitt/tmp/instances.txt' INTO TABLE instance; 
+
+SELECT * FROM storage_object LEFT JOIN instance ON storage_object.so_id=instance.so_id WHERE storage_object.ext_id = "foobarbaz1.fits";
+SELECT * FROM storage_object LEFT JOIN instance USING (so_id) WHERE storage_object.ext_id = "foobarbaz1.fits";
+
+SET AUTOCOMMIT = 0;
+BEGIN;
+UPDATE instance SET read_lock=read_lock+1 WHERE ins_id=1;
+INSERT INTO lock_record values (1, 'read', NULL);
+COMMIT;
+
+SET AUTOCOMMIT = 0;
+BEGIN;
+INSERT INTO storage_object values (NULL, 'myfooedbar.fits', 'mr. wizard made this', NULL);
+INSERT INTO instance values (NULL, LAST_INSERT_ID(), 'file://tmp/bat/man/12345a', 'da39a3ee5e6b4b0d3255bfef95601890afd80709',0, NULL, NULL);
+COMMIT;
+
+IF select write_lock from instance WHERE id=1 THEN
+ELSE
+ENDIF
Index: /branches/eam_branches/neb-host-20250516/docs/design.txt
===================================================================
--- /branches/eam_branches/neb-host-20250516/docs/design.txt	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/docs/design.txt	(revision 42864)
@@ -0,0 +1,283 @@
+# Copyright (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: design.txt,v 1.6 2005-11-05 01:13:35 jhoblitt Exp $
+
+Abstract
+--
+
+Nebulous is a user-space distributed object (file) storage system.  It is much
+more of a database of objects locations then a file system in the traditional
+sense.  Nebulous is considered distributed because of it's data storage model,
+where objects are stored across a number of storage volumes and may have one
+or more redundant copies.  While IPC is done in the traditional client/server
+model with a single[1] centralized server containing all storage object
+meta-data.  This system was designed, unlike most distributed storage system,
+specifically so that clients have the option of hosting one or more storage
+volumes and that data may be stored and accessed locally on a node.  This
+allows network I/O to be dramatically reduced if the end user does simple data
+locality optimizations.  Although, if you so choose, clients and storage
+volumes can reside on independent hardware.  Above all else, Nebulous is
+designed to make efficient use of both disk and network bandwidth.
+
+Data model
+--
+
+In Nebulous terminology a file is refereed to as a a resource.  Each resource
+is represented inside the Nebulous Server as a "Storage Object".  There is one
+Storage Object per resource (a file) that is being managed by the Nebulous
+Server.  A Storage Object maps an External Identifier (eg. filename) to a
+Storage ID.  In addition, creation time and comments may be stored in a
+Storage Object.  Storage Objects /do not/ contain the storage location of a
+resource as we need to allow for N copies of a resource to be stored.  The
+actual storage location of a resource is held in a Storage Object Instance,
+which maps to a Storage ID in a many-to-one relationship.
+
+A Storage Object Instance, or just Instance, represents a single copy of a
+resource being managed by the Nebulous Server. An Instance contains an
+Instance ID, the parent Storage ID, the location of the resource as a URI, and
+other assorted meta-data.
+
+Storage IDs are a signed 64-bit integer.  Negative values are considered
+reserved for future use.  Allocation should be incremental and, as we are
+unlikely to exceed 2^63 Storage Objects in the life time of the project, never
+reused. Instance IDs follow the same scheme as Storage IDs but exists in a
+unique namespace.
+
+Database record relationships
+--
+* All Storage Objects must have at least once associated Instance.
+
+* All Instances must be associated with a Storage Object.
+
+* All Instances must be associated with a valid URI (file exists).
+
+* All Storage Objects with locks set must have the correct number of
+corresponding Lock Records.
+
+* All Lock Records must be associated with a Storage Object.
+
+Data transport
+--
+XXX
+
+Data Storage
+--
+XXX
+
+
+Components
+--
+
+There are 4 principle components required for a working Nebulous system;
+server, client, data transport, and data storage.  The Nebulous software
+distribution provides only the server and the client software.  Data
+transport[2] and Data storage are left to 3rd party software packages.  As
+those functionalities are typically included with most operating systems.
+Nebulous build upon and benefits from advances made in those technologies.
+
+Server
+--
+
+The server is responsible for keeping track of storage objects, all instances
+of that object, and enforcing locking semantics.  Extensive logging and
+tracing support is provided for debug and to allow for statics generation and
+possible X<hotspot> optimization.
+
+Nebulous uses a centralized server model.  This model was choosen because it
+allows efficient X<pattern matching> of storage object names.  The current
+'best' technique for a distributed metadata store is with distributed hash
+tables.  Unfortunately, no widely available DHT implementation allows efficient
+I<pattern matching> of key names.
+
+
+[1] Multiple Nebulous servers should be possible via database replication.
+[2] A future version of Nebulous may include it's own data transport layer
+based on the WEB DAV protocol.
+
+Client <-> Server IPC
+--
+
+The Nebulous Server and clients communicate via the SOAP protocol.  Since SOAP
+is platform independent this allows native Nebulous clients to be written in a
+variety of languages.  With in the Nebulous Server itself the IPC mechanism is
+abstracted to allow ether multi-protocol support or complete replacement of
+SOAP by another protocol.
+
+XXX SOAP namespace
+
+
+Storage volume state tracking.
+
+Nebulous itself does not attempt to determine the state of storage volumes.
+Instead, it relies on some 3rd party to determine both the state
+(available/not) and capacity (total size + % utilized).  This information is
+then feed into Nebulous via either the administrative API or CLI management
+tools.
+
+XXX Storage Location determination
+
+
+Locking semantics
+--
+Nebulous uses 'advisory locking' where clients are excepted to check for the
+existence of locks but this policy is not enforced.  Locks are set on a
+Storage Object and apply to all Instances of that object.
+
+Two types of locks can be set; 'read' locks and 'write' locks.  Read locks are
+implemented as a semaphore while write locks use an exclusive mutex.
+
+    - Read lock
+In order to obtain a Read Lock on a Storage Object the Write Lock mutex must
+be in the state of 'free'.  When a Read Lock is acquire the Read semaphore is
+incremented by one.  When a Read Lock is released the Read semaphore is
+decremented by one.  This must happen atomically with the
+creation/destruction of a Lock Record.
+
+    - Write lock
+In order to obtain a Write lock on a Storage Object the Write Lock mutex must
+be in the state of 'free' and the Read lock semaphore must have a value of
+zero.  When a Write Lock is acquired the Write mutex is set to 'write'.  When a
+Write Lock is released the Write mutex is set to to 'free'.  This must happen
+atomically with the creation/destruction of a Lock Record.  As an extension, a
+sate of 'wait' may be implemented such that clients waiting on Read Locks to
+be released may prevent any additional Read Locks from being acquired.
+
+XXX Lock Records are currently unimplemented
+    - Lock Records A Lock Record is used to double check the indicated lock
+      state of a Storage Object and can be used to identify logical system
+errors.  A Lock Record contains the Storage ID of the object and the type of
+lock set.  eg, enum( 'read', 'write').
+
+    - Client best practices
+When attempting to acquire a lock a client should either "spin lock" with a
+reasonable pause between lock attempts or fail completely.  Aggressive "spin
+locking" is considered antisocial and may eventually require the Nebulous
+Server to identify and ignore such clients.
+
+
+House keeping
+--
+    - Lock sweeping
+In the event that a Storage Object operation fails to complete successfully
+stale locks will have to be identified and removed from the IPP Pixel Data
+Server Database.  This should be done periodically by comparing the entries
+in the Lock table to the list of active nodes maintained by the IPP
+Controller.  It should also happen as soon as possible after a node goes
+offline (triggered by the IPP Controller marking a node as offline?).  A sweep
+must be /completed/ before an offline node can be marked on-line.
+
+Once a node is determined to be offline all entries in the Lock table set by
+that node should be identified.   The locks on the Storage Object
+Instances pointed to by those entries should then be rolled back and Lock
+Record entries themselves must be removed from the lock table.
+
+    - Consistency sweeping
+Periodically the IPP Pixel Data Server meta-data and Storage Object will need
+to be checked for sanity.  This would be similar to running fsck on a modern
+filesystem.  Consistency sweeping should include Lock sweeping and should be
+considered a super-set.
+
+
+Server Operations
+--
+    - setup
+    - create_object
+    - rename_object
+        XXX unimplemented
+    - link_object
+        XXX unimplemented
+    - replicate object
+    - lock_object
+    - unlock_object
+    - find_objects
+        XXX unimplemented
+    - find_instances
+    - delete_instance
+    - fission_object
+        XXX unimplemented
+    - fuse_objects
+        XXX unimplemented
+    - stat_object
+    - stat_instance
+        XXX unimplemented
+
+Client Operations
+--
+    - create
+Creates and opens new Storage Object
+
+    - replicate
+Adds an Instance to a Storage Object
+
+    - cull
+Removes an Instance from a Storage Object
+
+This operation can not remove the last Instance of a Storage Object.
+
+    - lock
+Trys to acquire a lock on a storage object
+
+This operation times out after a default interval of 10s.
+
+    - unlock
+Trys to release a lock on a storage object
+    
+    - find_instances
+Find all instances of a storage object
+
+Only Instances on active storage volumes are found.
+
+    - find
+Find any instance of a storage object
+
+Only Instances on active storage volumes are found.
+
+    - open
+Open a storage object for read or write
+
+Opening a storage object as 'write' will remove all but one Instance to
+prevent Instances from becoming inconsistent.
+
+    - delete
+Delete a storage object and all of it's instances
+
+Removes a storage object and all of it's instances by sequentially deleting each
+Instance.  When a Storage Object has no more associated instances it is
+automatically removed by the Nebulous server.
+
+    - copy
+Copy a storage object
+
+The new storage object will be created with only one Instance.  Most properties
+of the source Storage Object will be preserved.
+
+    - move
+Rename a storage object
+
+Currently this operation will remove all but one instance of the storage
+object and may change it's storage location.
+
+    - delete_instance
+    
+Remove a storage object instance
+
+Removing the last Instance of a Storage Object will destroy the Storage
+Object.
+
+    - stat
+View the properties of a storage object
+
+
+    - import
+        XXX unimplemented
+Import a file into Nebulous
+
+Creates a new Storage Object with the specified External Identifier.  The
+source file will then be "copied" into the Nebulous system leaving the original
+file unmodified.
+
+    - fission
+        XXX unimplemented
+Create a new Storage Object from an existing one by either splitting off an
+Instance and re-parenting it or by cloning an existing Instance and associating
+it with the new Storage Object.
Index: /branches/eam_branches/neb-host-20250516/docs/install.txt
===================================================================
--- /branches/eam_branches/neb-host-20250516/docs/install.txt	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/docs/install.txt	(revision 42864)
@@ -0,0 +1,384 @@
+=pod
+
+=head1 Installing a Nebulous Server
+
+=head2 Create a UNIX group for Nebulous
+
+All Nebulous user's need to be in the same UNIX group as the Nebulous server.
+
+    groupadd -g 877 nebulous
+
+This is a silly hack to generate a gid.
+
+    perl -le 'map { $gid += ord } split //, shift; print $gid' nebulous
+
+=head2 Add the users that you want to be able to access Nebulous to that group
+
+    usermod -G nebulous foouser
+
+=head2 Setup directory permissions for the nebulous group
+
+As root:
+
+    mkdir /data/ipp000.0/nebulous
+    mkdir /data/ipp000.1/nebulous
+    mkdir /data/ipp002.0/nebulous
+    mkdir /data/ipp003.0/nebulous
+
+    # note that this is just an example, this won't work over NFS with
+    # root_squash enablded
+    chown nobody:nebulous /data/ipp00?.?/nebulous
+
+    chmod 0770 /data/ipp00?.?/nebulous
+    ls -lad /data/ipp00?.?/nebulous
+
+    mkdir /data/`hostname`.0/nebulous
+    chown nobody:nebulous /data/`hostname`.0/nebulous
+    chmod 0770 /data/`hostname`.0/nebulous
+
+=head2 Build and install the Nebulous Perl modules (see: README)
+
+=head2 Create and initialize the database
+
+    mysql -u root mysql -p
+    (enter your MySQL root password)
+
+    DROP DATABASE IF EXISTS nebulous;
+    CREATE DATABASE nebulous;
+    GRANT ALL PRIVILEGES ON nebulous.* TO 'nebulous'@'localhost'
+        IDENTIFIED BY '@neb@';
+    FLUSH PRIVILEGES;
+    exit
+
+    export NEB_DB=nebulous
+    export NEB_USER=nebulous
+    export NEB_PASS=@neb@
+
+    *** this is very dangerous and should only be run in test examples:
+    neb-initdb
+
+    neb-voladd --vhost ipp000 --vname ipp000.0 --uri file:///data/ipp000.0/nebulous --mountpoint /data/ipp000.0/nebulous
+    neb-voladd --vhost ipp001 --vname ipp000.1 --uri file:///data/ipp000.1/nebulous --mountpoint /data/ipp000.1/nebulous
+    neb-voladd --vhost ipp002 --vname ipp002.0 --uri file:///data/ipp002.0/nebulous --mountpoint /data/ipp002.0/nebulous
+    neb-voladd --vhost ipp003 --vname ipp003.0 --uri file:///data/ipp003.0/nebulous --mountpoint /data/ipp003.0/nebulous
+    neb-voladd --vhost ipp022 --vname ipp022.0 --uri file:///data/ipp022.0/nebulous --mountpoint /data/ipp022.0/nebulous
+
+    # or in bulk:
+    
+    for i in ipp005 ipp006 ipp007 ipp009 ipp010 ipp011 ipp012 ipp013 ipp014 ipp015 ipp016 ipp017 ipp018 ipp020 ipp021 ipp022 ipp023 ipp024 ipp025 ipp026 ipp027 ipp028 ipp029 ipp030 ipp031 ipp032 ipp033 ipp034 ipp035 
+    do 
+        /usr/bin/neb-voladd --vname ${i}.0 --vhost $i --uri file:///data/${i}.0/nebulous_beta
+    done
+
+#    perl -e 'for (1..24) { $i = sprintf "po%02d", $_ ; system "neb-voladd
+#    --vname $i --vhost $i --uri file:/$i/nebulous" }'
+    .
+    .
+
+If you get an error similar to this while initializing the database it means
+that the creation of the Nebulous's stored procedures failed:
+
+    ERROR 1146 (42S02): Table 'mysql.proc' doesn't exist
+
+It probably means that your instance of MySQL has been upgraded for an older
+version that did not support stored precedures.  You can fix this by running
+the C<mysql_fix_privilege_tables> script that you should have received with
+MySQL.
+
+In order for the test suite to pass the "test" account needs have the
+appropriate permissions to create and modify stored procedures.
+
+    update user set Create_routine_priv = 'Y', Alter_routine_priv = 'Y', Execute_priv = 'Y' where user = 'test';
+    update db set Create_routine_priv = 'Y', Alter_routine_priv = 'Y', Execute_priv = 'Y' where db = 'test';
+
+    flush privileges;
+
+Please see this webpage for further details:
+
+http://dev.mysql.com/doc/refman/5.0/en/stored-procedure-privileges.html
+
+
+=head2 Setup nebdiskd daemon
+
+Create the .netdiskrc file in the home directory of the user that nebdiskd
+will run as.  This does not need to be/should not be the root user.  Merely a
+user with permissions to see all of the approparite volumes.
+
+Example F<.netdiskrc>:
+
+    db: nebulous
+    dbpass: '@neb@'
+    dbuser: nebulous
+    mounts:
+      - /data/ipp000.0
+      - /data/ipp000.1
+      - /data/ipp002.0
+      - /data/ipp003.0
+    pidfile: /var/tmp/nebdiskd
+    poll_interval: 5
+
+Start the daemon by running the command C<nebdiskd>.  Use this command to
+verify that the daemon is working correclty.
+
+    neb-volstat
+
+nebdiskd should always been running when nebulous is use.  This command is an
+example of how to startup the daemon running as another user.  This could be
+use to start the daemon from the root account while the system init scripts
+are running.
+
+    sudo -H -u jhoblitt nebdiskd
+
+See the nebdiskd POD/man page for further details
+
+=head2 Install Apache2 with mod_perl
+
+=head3 Minimum software requirements:
+
+Nebulous will work with both Apache 1.3 & 2.x.  However, this guide will only
+cover configuraton with Apache 2.x and mod_perl 2.x.
+
+=over 4
+
+=item * Apache2 >= 2.0.54
+
+=item * mod_perl >= 2.0.0
+
+=item * Apache::DBI >= '0.97' [*]
+
+=back
+
+[*] required for connect_on_init() to work under mod_perl >= 2.0.1
+
+=head3 Apache 1.x 
+
+=item * building mod_perl-1.29 from source:
+
+    perl Makefile.PL USE_APXS=1 INSTALLDIRS=vendor WITH_APXS=/usr/sbin/apxs EVERYTHING=1 PERL_DEBUG=1
+
+    #MP_TRACE = 1 MP_DEBUG = 1 MP_USE_DSO = 1 MP_INST_APACHE2 = 1 MP_APXS = /usr/sbin/apxs2
+
+=head3 Apache 2.x 
+
+=head4 removing Apache on RHEl3
+
+On RHEL3 I had to remove these packages in order to fully remove MySQL.
+
+As C<root>:
+
+    rpm -e \
+    httpd-2.0.46-46.2.ent \
+    httpd-devel-2.0.46-46.2.ent \
+    redhat-config-httpd-1.1.0-4.30.2 \
+    mod_perl-1.99_09-10.ent \
+    mod_authz_ldap-0.22-5 \
+    mod_auth_pgsql-2.0.1-4.ent \
+    mod_python-3.0.3-5.ent \
+    mod_auth_mysql-20030510-2.ent \
+    mod_ssl-2.0.46-46.2.ent \
+    webalizer-2.01_10-15.ent \
+    mod_perl-1.99_09-10.ent \
+    mod_auth_pgsql-2.0.1-4.ent \
+    mod_python-3.0.3-5.ent \
+    mod_auth_mysql-20030510-2.ent \
+    mod_ssl-2.0.46-46.2.ent \
+    squirrelmail-1.4.3a-11.EL3 \
+    php-4.3.2-25.ent \
+    php-imap-4.3.2-25.ent \
+    php-ldap-4.3.2-25.ent \
+    php-mysql-4.3.2-25.ent \
+    php-odbc-4.3.2-25.ent \
+    php-pgsql-4.3.2-25.ent 
+
+    rm -rf /etc/httpd
+
+=head4 installing Apache2 from source
+
+    tar -jxvf httpd-2.0.54.tar.bz2
+    cd httpd-2.0.54
+    ./configure \
+    --disable-suexec \
+    --with-perl=/usr/bin/perl \
+    --with-port=80 \
+    --with-program-name=apache2 \
+    --with-devrandom=/dev/urandom \
+    --prefix           /usr \
+    --exec_prefix      /usr \
+    --bindir           /usr/bin \
+    --sbindir          /usr/sbin \
+    --libdir           /usr/lib \
+    --libexecdir       /usr/lib/apache2/modules \
+    --mandir           /usr/share/man \
+    --infodir          /usr/share/info \
+    --includedir       /usr/include/apache2 \
+    --datadir          /var/www/localhost \
+    --sysconfdir       /etc/apache2/conf \
+    --localstatedir    /var
+    make
+    su
+    make install
+    exit
+    cd ..
+
+=head4 installing mod_perl 2.x
+
+    tar -zxvf mod_perl-2.0.1.tar.gz
+    cd mod_perl-2.0.1
+    perl Makefile.PL
+    (apxs = /usr/sbin/apxs)
+    make
+    su
+    make install
+    echo "LoadModule perl_module /usr/lib/apache2/modules/mod_perl.so" >> /etc/apache2/conf/apache2.conf
+    exit
+
+    apachectl configtest
+    Syntax OK
+
+=head4 installing and configuring Nebulous
+
+    tar -zvxf Nebulous-0.01.tar.gz
+    cd Nebulous-0.01
+    perl Build.PL -axps /usr/sbin/apxs
+    ./Build
+    ./Build test
+    su
+    ./Build install
+
+
+    vi /etc/apache2/conf/startup.pl
+    (change Group to "Group nebulous")
+
+    cp fooconf /etc/apache2/conf/startup.pl
+    echo "PerlPostConfigRequire /etc/apache2/conf/startup.pl" >> /etc/apache2/conf/apache2.conf
+
+
+=head3 configurating Apache to act as a Nebulous/SOAP server
+
+=item * add the "nebulous" group to /etc/group
+=item * configure Apache to run as the nebulous group. 
+
+    Group nebulous
+
+Note that in theory the C<perchild> MPM can run vhosts as differenet
+users/groups but this module is currently non-funcitonal.
+
+L<http://httpd.apache.org/docs/trunk/mod/perchild.html>
+
+=item * configure a path for Neublous
+
+Make sure mod_perl is being loaded into Apache.  In order to do this on
+gentoo, you must edit /etc/config.d/apache2 and add C<-DPERL> to
+C<APACHE2_OTPS>.
+
+    cat <<END >> /etc/apache2/conf/apache2.conf
+    <Location /nebulous>
+        SetHandler perl-script
+        PerlResponseHandler Apache2::SOAP
+        PerlSetVar dispatch_to "PerlHandler Nebulous::Server::SOAP"
+        PerlSetVar options "compress_threshold => 10000"
+    </Location>
+    END
+
+    apachectl configtest
+    Syntax OK
+
+=item * initialize Nebulous database connections from C<startup.pl>
+
+    my $dsn         = 'DBI:mysql:database=nebulous:host=localhost';
+    my $dbuser      = 'nebulous';
+    my $dbpasswd    = '@neb@';
+
+    Apache::DBI->connect_on_init( $dsn, $dbuser, $dbpasswd );
+    Nebulous::Server::SOAP->new_on_init(
+        dsn         => $dsn,
+        dbuser      => $dbuser,
+        dbpasswd    => $dbpasswd,
+        log_level   => 'all',
+    );
+
+# out of date from here down...
+
+#
+# CGI interface
+#
+
+ScriptAlias /nebulous /usr/local/apache/perl/imageserver.pl
+
+<Directory "/usr/local/apache/perl">
+        AllowOverride None
+        Options None
+        Order allow,deny
+        Allow from all
+</Directory>
+
+PerlModule Apache::PerlRun
+<Location /perl>
+        SetHandler perl-script
+        PerlHandler Apache::PerlRun
+        Options ExecCGI
+        allow from all
+        PerlSendHeader On
+</Location>
+
+#
+# Nebulous::Apache interface
+#
+
+PerlModule Apache::DBI
+PerlModule Nebulous::Apache
+
+<Location /nebulous>
+        SetHandler perl-script
+        PerlHandler Nebulous::Apache
+</Location>
+
+- setup the server
+
+As a CGI, will with either Apache 1.3.x or 2.0.x
+
+cp scripts/imagesever.pl /var/www/localhost/cgi-bin
+chmod 755 /var/www/localhost/cgi-bin/imageserver.pl
+touch /tmp/imageserver.log
+chmod 1777 /tmp/imageserver.log
+
+=head2 Testing the Nebulous Server
+
+You can test your configuration with the C<telnet> utility.
+
+    $ telnet localhost 80
+    Trying 127.0.0.1...
+    Connected to localhost.localdomain (127.0.0.1).
+    Escape character is '^]'.
+    GET /nebulous HTTP/1.0
+
+    HTTP/1.1 500 Internal Server Error
+    Date: Wed, 31 Aug 2005 01:10:47 GMT
+    Server: Apache/2.0.54 (Unix) mod_perl/2.0.1 Perl/v5.8.6
+    Content-Length: 645
+    Connection: close
+    Content-Type: text/html; charset=iso-8859-1
+    .
+    .
+
+    Connection closed by foreign host.
+
+If you get an HTTP 5xx code check your apache error_log
+
+
+=head2 MySQL Configuration/tuning
+
+in /etc/mysql/my.cnf
+under [mysqld]
+increase the number of alloweed connections, e.g.
+max_connections = 200
+
+Nebulous makes heavy use of MySQL's C<innodb> table type.  Try configuring
+MySQL with these parameters:
+
+    innodb_log_files_in_group = 2
+    innodb_buffer_pool_size = as big as possible (512M+)
+    innodb_log_buffer_size= 16M
+    innodb_log_file_size=10M
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/docs/requirements.txt
===================================================================
--- /branches/eam_branches/neb-host-20250516/docs/requirements.txt	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/docs/requirements.txt	(revision 42864)
@@ -0,0 +1,28 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id: requirements.txt,v 1.2 2005-09-14 00:46:01 jhoblitt Exp $
+
+- store all image data aquired during the first two years of PS1's
+  operations.  Approximately 10^8 files (OTA level).
+
+- injest new data from the summit at the same rate it is produced when
+  operating at a cadence of 40s between integrations.  Approximately 400Mb/s
+  and 64 files/integration.
+
+- support phase 1 & 2 processing. x files per t_min
+
+- support phase 3 & 4 processing. x files per t_min
+
+- support accessing small blocks of a file (ie., seek)
+
+- tolerate partal system failures without data loss
+
+- execeedly efficent use of both disk and network I/O
+
+what are the science drivers for these 3?
+--
+- data corruption is guarenteed to be detectable.
+
+- relibably store xx.x% of files 
+
+- xx.x% of stored images must be retreivable in xs.
Index: /branches/eam_branches/neb-host-20250516/examples/Makefile
===================================================================
--- /branches/eam_branches/neb-host-20250516/examples/Makefile	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/examples/Makefile	(revision 42864)
@@ -0,0 +1,5 @@
+NEB_CFLAGS=`pkg-config nebclient --cflags`
+NEB_LIBS=`pkg-config nebclient --libs`
+
+nebexample: nebexample.c
+	$(CC) $(NEB_CFLAGS) $(CFLAGS) $(NEB_LIBS) $< -o $@
Index: /branches/eam_branches/neb-host-20250516/examples/nebexample.c
===================================================================
--- /branches/eam_branches/neb-host-20250516/examples/nebexample.c	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/examples/nebexample.c	(revision 42864)
@@ -0,0 +1,45 @@
+// gcc `pkg-config nebclient --cflags --libs` example.c -o example
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <nebclient.h>
+
+#define NEB_SERVER  "http://podb1.ifa.hawaii.edu:80/nebulous"
+#define NEB_KEY     "foobarbaz"
+
+int main (int argc, char **argv) {
+    nebServer       *server = NULL;
+    char            *key = NEB_KEY;
+
+    server = nebServerAlloc(NEB_SERVER);
+    if (!server) {
+        printf("nebServerAlloc() failed\n");
+        exit(EXIT_FAILURE);
+    }
+
+    // make sure there isn't already a file named "foobarbaz" so this example
+    // doesn't cause an error
+    nebDelete(server, key);
+
+    int fh = nebCreate(server, key, 0, NULL, NULL, NULL);
+    if (fh < 0) {
+        printf( "nebCreate() failed: %s\n", nebErr(server));
+        exit(EXIT_FAILURE);
+    }
+
+    // fh is a file descriptor
+    close(fh);
+
+    if (!nebReplicate(server, key, NULL, NULL)) {
+        printf( "nebReplicate() failed: %s\n", nebErr(server));
+        exit(EXIT_FAILURE);
+    }
+
+    if (!nebDelete(server, key)) {
+        printf( "nebDelete() failed: %s\n", nebErr(server));
+        exit(EXIT_FAILURE);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /branches/eam_branches/neb-host-20250516/examples/nebexample.pl
===================================================================
--- /branches/eam_branches/neb-host-20250516/examples/nebexample.pl	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/examples/nebexample.pl	(revision 42864)
@@ -0,0 +1,28 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Nebulous::Client;
+
+my $neb = Nebulous::Client->new(
+    proxy   => 'http://podb1.ifa.hawaii.edu:80/nebulous'
+);
+
+my $key = shift || 'foobarbaz';
+
+# make sure there isn't already a file named "foobarbaz" so this example
+# doesn't cause an error
+$neb->delete($key);
+
+my $fh = $neb->create($key);
+die "can't create file $key " unless $fh;
+close($fh);
+
+if (!$neb->replicate($key)) {
+    die "can't replicate object $key";
+}
+
+if (!$neb->delete($key)) {
+    die "can't delete object $key";
+}
Index: /branches/eam_branches/neb-host-20250516/examples/uri_test.pl
===================================================================
--- /branches/eam_branches/neb-host-20250516/examples/uri_test.pl	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/examples/uri_test.pl	(revision 42864)
@@ -0,0 +1,18 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use URI;
+
+my $u1 = URI->new("neb://www.perl.com/bar");
+
+print $u1->scheme, "\n";
+print $u1->opaque, "\n";
+print $u1->path, "\n";
+
+my $u2 = URI->new("neb:/www.perl.com");
+print $u2->scheme, "\n";
+print $u2->opaque, "\n";
+print $u2->path, "\n";
+
Index: /branches/eam_branches/neb-host-20250516/init.d/nebdiskd
===================================================================
--- /branches/eam_branches/neb-host-20250516/init.d/nebdiskd	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/init.d/nebdiskd	(revision 42864)
@@ -0,0 +1,20 @@
+#!/sbin/runscript
+
+depend() {
+        need net
+        use logger
+}
+
+start() {
+# archttp looks for and write in config file under /.  as I can tell there is
+# no way to specify the path to the config file.
+        ebegin "Starting nebdiskd"
+        /usr/bin/nebdiskd 
+        eend $?
+}
+
+stop() {
+        ebegin "Stopping nebdiskd"
+        /usr/bin/nebdiskd --stop 
+        eend $?
+}
Index: /branches/eam_branches/neb-host-20250516/lib/Nebulous/Key.pm
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Nebulous/Key.pm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Nebulous/Key.pm	(revision 42864)
@@ -0,0 +1,282 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Keys.pm,v 1.3 2008-09-09 02:18:47 jhoblitt Exp $
+
+package Nebulous::Key;
+
+use strict;
+use warnings FATAL => qw( all );
+
+our $VERSION = '0.03';
+
+use base qw( Exporter Class::Accessor::Fast );
+
+use File::Spec;
+use URI::file;
+use Carp qw( croak );
+use URI;
+use overload '""' => \&_stringify_key;
+
+our @EXPORT_OK = qw(
+    parse_neb_key
+    parse_neb_volume
+);
+
+__PACKAGE__->mk_ro_accessors(qw( path volume hard_volume ));
+
+sub parse_neb_key
+{
+    my ($key, $volume) = @_;
+
+    croak "key param is not optional" unless defined $key;
+    croak "too many params" if scalar @_ > 2;
+
+    # white space is not allowed
+    if ($key =~ qr/\s+/) {
+        croak "keys and URIs may not contain whitespace";
+    }
+
+    my $uri = URI->new($key);
+    my $scheme = $uri->scheme;
+    my $path = $uri->opaque;
+    
+    my $volume_name;
+    my $hard_volume;
+    # if this is a valid uri 
+    if (defined $scheme) {
+        # if so, does it use the neb scheme?
+        croak "URI does not use the 'neb' scheme"
+            unless $scheme eq 'neb'; 
+
+        # neb:path (not allowed)
+        # neb://<volume name>/...
+        # neb:/path... (leading '/' is stripped)
+        # neb:///path... (same as neb:/path)
+
+        # volume specifier must be at least one character
+        # strip off volume component if it exists
+        $path =~ s|^//([^/]+)||;
+
+        # a new is not allowed to be just a volume specifier, it must have a
+        # path component to it
+        unless (length $path) {
+            croak "neb URI scheme requires a path component"; 
+        }
+        
+        # ignore key supplied volume name if one is passed as a parameter
+        $volume ||= $1;
+        my $volume_info = parse_neb_volume($volume);
+
+        # magically eat the "any" volume
+        if (defined $volume_info->{volume} and $volume_info->{volume} ne 'any') {
+            $volume_name = $volume_info->{volume};
+            $hard_volume = $volume_info->{hard_volume};
+        }
+
+        # require a leading slash if there is no volume name
+        if ((not defined $volume_name) and (not $path =~ m|^/|)) {
+            croak "neb URI scheme requires a leading slash, eg. neb:/";
+        }
+    } else {
+        my $volume_info = parse_neb_volume($volume);
+
+        # magically eat the "any" volume
+        if (defined $volume_info->{volume} and $volume_info->{volume} ne 'any') {
+            $volume_name = $volume_info->{volume};
+            $hard_volume = $volume_info->{hard_volume};
+        }
+    }
+
+    # strip leading slashes
+    $path =~ s|^/+||;
+
+    # strip leading '.' or '..'
+    $path =~ s|^\.{1,2}||;
+
+    # remove multiple /'s and trailing slashes
+    $path = File::Spec->canonpath($path);
+
+    return __PACKAGE__->new({
+        volume      => $volume_name,
+        hard_volume => $hard_volume,
+        path        => $path,
+    });
+}
+
+
+sub parse_neb_volume
+{
+    my $volume = shift;
+    return unless defined $volume;
+
+    my $hard_volume;
+    # check to see if there is a tilde and remove it if found
+    if (defined $volume and $volume =~ s/^~//) {
+        $hard_volume = 1;
+    }
+
+    return({ volume => $volume, hard_volume => $hard_volume });
+}
+
+
+sub _stringify_key
+{
+    my $self = shift;
+
+    my $path        = $self->path;
+    my $volume      = $self->volume || '';
+    my $hard_volume = $self->hard_volume ? '~' : '';
+
+    return "neb://${hard_volume}${volume}/$path";
+}
+
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+Nebulous::Key - Nebulous Keys Explained
+
+=head1 DESCRIPTION
+
+The Nebulous system interprets it's storage object keys in a I<slightly>
+magical manner.  It supports both plain vanilla C<key strings> and keys in the
+form of an L<URI>.  In the L<URI> form a specific storage volume can be implied
+(as a request, not a requirement).  Both key forms are subject to mangling such
+that I<IF> the key I<looks> like a C<POSIX> semantics file path, it is
+canocalized.
+
+=head1 RESERVED SYNTAX
+
+This particular syntax is disallowed and is reserved for future use.
+
+    <neb:<phrase>/<path>
+
+=head1 EXAMPLES
+
+=head2 Key Strings
+
+    key:        foo/bar/baz/quix
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+    key:        /foo/bar/baz/quix
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+    key:        //foo/bar/baz/quix
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+    key:        ///foo/bar/baz/quix
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+    key:        foo////bar/baz/quix
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+    key:        foo/bar/baz/quix/
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+=head2 URI w/ volume name
+
+neb://<volume>/<path>
+
+    key:        neb://foo/bar/baz/quix
+    mangled to: bar/baz/quix
+    volume:     foo
+
+    key:        neb://foo//bar/baz/quix
+    mangled to: bar/baz/quix
+    volume:     foo
+
+    key:        neb://foo///bar/baz/quix
+    mangled to: bar/baz/quix
+    volume:     foo
+
+    key:        neb://foo/bar///baz/quix
+    mangled to: bar/baz/quix
+    volume:     foo
+
+    key:        neb://foo/bar/baz/quix/
+    mangled to: bar/baz/quix
+    volume:     foo
+
+=head2 URI w/o volume name
+
+neb:/<path>
+
+    key:        neb:/foo/bar/baz/quix
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+    key:        neb:///foo/bar/baz/quix
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+    key:        neb://///foo/bar/baz/quix
+    mangled to: foo/bar/baz/quix
+    volume:     any
+
+=head2 Forbidden Keys
+
+=head3 Key with whitespace
+
+    "/ foo/bar/baz/quix"
+    " /foo/bar/baz/quix"
+    "/foo/bar/baz/quix "
+
+=head3 URI with whitespace
+
+    "neb ://foo/bar/baz/quix"
+    "neb:// foo/bar/baz/quix"
+    " neb://foo/bar/baz/quix"
+    "neb://foo/bar/baz/quix "
+
+=head3 URI with out a volume requires a leading slash
+
+    neb:foo/bar/baz/quix
+
+=head1 CREDITS
+
+Just me, myself, and I.
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=head1 COPYRIGHT
+
+Copyright (C) 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 LICENSE file included with
+this module, or in the L<perlgpl> Pod as supplied with Perl 5.8.1 and later.
+
+=head1 SEE ALSO
+
+L<Nebulous::Server>, L<Nebulous::Client>, L<nebclient(3)>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server.pm
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server.pm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server.pm	(revision 42864)
@@ -0,0 +1,2913 @@
+# Copyright (c) 2004-2008  Joshua Hoblitt
+#
+# $Id: Server.pm,v 1.96 2008-12-14 22:54:25 eugene Exp $
+
+package Nebulous::Server;
+
+use strict;
+use warnings FATAL => qw( all );
+no warnings qw( uninitialized );
+
+our $VERSION = '0.18';
+
+use base qw( Class::Accessor::Fast );
+
+use Carp;
+
+use Cache::Memcached;
+use DBI;
+
+## for gentoo:
+# use Digest::SHA1 qw( sha1_hex );
+
+## for ubuntu:
+use Digest::SHA qw( sha1_hex );
+
+use Fcntl ':mode';
+use File::Basename qw( basename dirname fileparse );
+use File::ExtAttr qw( setfattr );
+use File::Path;
+use File::Spec;
+use Log::Log4perl qw( :levels );
+use Nebulous::Key qw( parse_neb_key parse_neb_volume );
+use Nebulous::Server::Config;
+use Nebulous::Server::Log;
+use Nebulous::Server::SQL;
+use Params::Validate qw( validate validate_pos SCALAR SCALARREF UNDEF BOOLEAN );
+use URI::file;
+
+__PACKAGE__->mk_accessors(qw( log sql config cache ));
+
+use constant SUBPATH_DEPTH  	=> 2;
+use constant NFS_RETRIES    	=> 100;
+use constant NFS_RETRY_WAIT 	=> 1;
+use constant TRANS_RETRY_WAIT 	=> 1;
+
+# This is the umask hack.
+umask(0002);
+
+# This determines how many entries from the list of volumes sorted by free space are randomized.
+my $topfew_count = 15;
+my $max_used_space = 0.98;
+# transaction restart/retry regex
+my $trans_regex = qr/Deadlock Found|Lock wait timeout exceeded|try restarting transaction|Can't connect to MySQL server/i;
+
+sub new
+{
+    my $class = shift;
+
+    # let Nebulous::Server::Config validate our params
+    my $config = Nebulous::Server::Config->new( @_ );
+
+    return $class->new_from_config($config);
+}
+
+
+sub new_from_config
+{
+    my $class = shift;
+
+    my ($config) = @_;
+
+    # log4perl is not available until we call init()
+    Nebulous::Server::Log->init($config);
+    my $log = Log::Log4perl::get_logger( "Nebulous::Server" );
+    $log->level($config->trace);
+    $log->debug( "entered - @_" );
+
+    my $sql = Nebulous::Server::SQL->new;
+
+    my $self = bless {}, ref $class || $class;
+    $self->log($log);
+    $self->sql($sql);
+    $self->config($config);
+    $self->cache(
+        Cache::Memcached->new({
+	    servers => $config->memcached_servers,
+			      })
+	);
+#    $self->cache->set("foo", "bar") or die "set failed";
+#    $self->cache->get("foo") or die "get failed";
+    $log->logdie("at least one database must be defined") unless $config->n_db;
+
+    # cause a db session to be started
+    $self->_db_for_index(0);
+
+    $log->debug( "leaving" );
+
+    return $self;
+}
+
+# EAM : In the 'distributed' version of Nebulous, there is a collection of N databases
+# the db_index uniquely defines the db used by a given key
+sub _db_index_for_key
+{
+    my $self = shift;
+
+    my $log     = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($key) = @_;
+
+    my $config  = $self->config;
+
+    my $db_index = 0;
+    $log->logdie("key not defined") unless defined $key;
+
+    # hash the key to select the correct database instance
+    # only use the first 8 hex chars... have to be careful to avoid an int
+    # overflow here
+
+    # hash only the directory component of the path and not the filename
+    my $path = dirname($key->path);
+    $db_index = unpack("h8", sha1_hex($path)) % $config->n_db;
+
+    $log->debug("index is $db_index");
+    $log->debug("leaving");
+
+    return $db_index;
+}
+
+sub _db_for_index
+{
+    my $self = shift;
+
+    my $log     = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($db_index) = @_;
+
+    my $sql     = $self->sql;
+    my $config  = $self->config;
+
+    # lookup to see if we have a stored dbh for this database
+    my $dbh = $self->{dbs}[$db_index];
+    # if the dbh is still alive, return it
+    if (defined $dbh and $dbh->ping) {
+        $log->debug("db handle is still alive");
+        return $dbh;
+    }
+    # otherwise create a new connection
+    $log->debug("db handle is dead/unopened");
+
+    # lookup database info
+    my $db_config = $config->db($db_index);
+    $log->logdie("can't find database configuration info for db # $db_index")
+        unless $db_config;
+
+    # if we're running under mod_perl & Apache::DBI is loaded we want to
+    # reconnect to the database everytime the dbh is requested.  The rational is
+    # that if we're running under mod_perl this is probably a log running
+    # processes and the database might have gone away on us.  Apache::DBI will
+    # take care of getting a valid dbh back.
+    TRANS: while (1) {
+        eval {
+            $dbh = DBI->connect_cached(
+                $db_config->dsn,
+                $db_config->dbuser,
+                $db_config->dbpasswd,
+                {
+                    RaiseError => 1,
+                    PrintError => 0,
+                    AutoCommit => 0,
+                },
+            );
+
+            $dbh->do( $sql->set_transaction_model );
+            $log->debug( "connected to database: ", sub { $dbh->data_sources; } );
+            $dbh->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $dbh->rollback if $dbh;
+            $log->debug("rollback") if $dbh;
+            if ($@ =~ qr/Can't connect to MySQL server/) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie( "database error: $@" );
+        }
+        last;
+    }
+
+    $self->{dbs}[$db_index] = $dbh;
+
+    $log->debug("leaving");
+
+    return $dbh;
+}
+
+sub db
+{
+    my $self = shift;
+
+    my $log     = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($key) = validate_pos(@_,
+        {
+            isa => 'Nebulous::Key',
+        },
+    );
+
+    my $sql     = $self->sql;
+    my $config  = $self->config;
+
+    $log->logdie("key not defined") unless defined $key;
+    my $db_index = $self->_db_index_for_key($key);
+
+    my $dbh = $self->_db_for_index($db_index);
+
+    return $dbh;
+}
+
+my $get_storage_volume_calls = 0;
+
+sub create_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($key, $vol_name) = validate_pos(@_,
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR|UNDEF,
+#            callbacks   => {
+#                # check that the volume requested is valid
+#                'is valid volume name' => sub {
+#                    return 1 if not defined $_[0];
+#                    $self->_is_valid_volume_name($_[0])
+#                },
+#            },
+            optional    => 1,
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # vol_name overrides the key implied volume
+    eval {
+        $key = parse_neb_key($key, $vol_name);
+    };
+    $log->logdie("$@") if $@;
+    $vol_name = $key->volume;
+
+    my $db  = $self->db($key);
+
+    # 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, $key->volume)) {
+        unless ($key->hard_volume) {
+	    # EAM : lower verbosity level
+            $log->debug( "$vol_name is not a known volume name" );
+            $vol_name = undef;
+        } else {
+           $log->logdie("$vol_name is not a valid volume name");
+        }
+    }
+        
+    $get_storage_volume_calls = 0;
+
+    my ($vol_id, $vol_host, $vol_path, $vol_xattr)
+        = $self->_get_storage_volume($key, $vol_name, $key->hard_volume);
+
+    my $parent_id = $self->_resolve_dir_parent_id(key => $key, create => 1);
+
+    my $uri;
+TRANS: while (1) {
+        eval {
+            {
+                # create storage_object
+                my $query = $db->prepare_cached( $sql->new_object ); 
+# bad syntax    $query->execute('NULL', $key->path, basename($key->path), $parent_id);
+                $query->execute(0, $key->path, basename($key->path), $parent_id);
+                $query->finish;
+            }
+
+            my $so_id;
+            {
+                # get object ID
+                my $query = $db->prepare_cached( $sql->last_insert_id );
+                $query->execute;
+                ($so_id) = $query->fetchrow_array;
+                # XXX finish seems to be required when using LAST_INSERT_ID() or we
+                # get a warning about the stmt handling still be active the next
+                # time LAST_INSERT_ID() is invoked
+                $query->finish;
+            }
+
+            {
+                # create storage_object_attr
+                my $query = $db->prepare_cached( $sql->new_object_attr ); 
+                $query->execute($so_id);
+                $query->finish;
+            }
+
+            {
+                
+                # create instance with no URI
+#            my $query = $db->prepare_cached( $sql->new_instance );
+                my $query = $db->prepare_cached( $sql->new_object_instance );
+                $query->execute($vol_id);
+                $query->finish;
+            }
+
+            my $ins_id;
+            {
+                # get instance ID
+                my $query = $db->prepare_cached( $sql->last_insert_id );
+                $query->execute;
+                ($ins_id) = $query->fetchrow_array;
+                # XXX finish seems to be required when using LAST_INSERT_ID() or we
+                # get a warning about the stmt handling still be active the next
+                # time LAST_INSERT_ID() is invoked
+                $query->finish;
+            }
+
+            # Unfortunately, since we want to use the instance row's ID as part of the
+            # actual on disk file name we can't try to create the file until after
+            # we've create both a new storage_storage object and instance.
+
+            # TODO add some stuff here to retry if unsucessful
+            $uri = $self->_create_empty_instance_file($key, $so_id, $ins_id, $vol_path, $vol_xattr);
+            $log->debug("created $uri on volume ID: $vol_id");
+
+            {
+                # update the instance with URI & vol_id that the file is on
+                my $query = $db->prepare_cached( $sql->update_instance_uri );
+                # vol_id, uri, ins_id
+                $query->execute($vol_id, "$uri", $ins_id);
+                $query->finish;
+            }
+
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+#        and not $key->soft_volume
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("error: $@");
+        }
+        last;
+    }
+
+    # add new key to the cache
+    $self->cache->set($key->path, 1) if defined $self->cache;
+    $log->debug( "key added to cache" );
+
+    $log->debug("leaving");
+
+    return "$uri";
+}
+
+
+sub _resolve_dir_parent_id
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my %p = validate(@_,
+        {
+            key     => {
+                isa         => 'Nebulous::Key',
+            },
+            create  => {
+                type        => BOOLEAN,
+                optional    => 1,
+                default     => undef,
+            },
+            # return dir_id instead of parent_id
+            dir_id  => {
+                type        => BOOLEAN,
+                optional    => 1,
+                default     => undef,
+            },
+        }
+    );
+
+    my $key = $p{key};
+
+    my $sql = $self->sql;
+    my $db  = $self->db($key);
+
+    # no path means '/', which has a dir_id & parent_id of 1
+    return 1 if $key->path eq '';
+
+    # resolve parent directory
+    my @dirs;
+
+    # File::Spec->splitpath was causing ->splitdir to always an extra dir
+    # named "" because of a trailing /
+    unless ($p{dir_id}) {
+        @dirs = File::Spec->splitdir(dirname($key->path));
+    } else {
+        @dirs = File::Spec->splitdir($key->path);
+    }
+    # dirname returns "." if there is no dir component to the path, we have
+    # to filter this out
+    @dirs = grep(!/^\.$/, @dirs);
+
+    $log->debug("looking for dirs - ", join(" : ", @dirs), "\n");
+
+    # start at the root dir; '/' == 1
+    my $parent_id = 1;
+    my $dir_id;
+TRANS: while (1) {
+        eval {
+            foreach my $dir (@dirs) {
+                $dir_id = undef;
+                {
+                    my $query = $db->prepare_cached($sql->get_directory); 
+                    $query->execute($parent_id, $dir);
+                    if ($query->rows) {
+                        $dir_id = $query->fetchrow_hashref->{'dir_id'};
+                        $log->debug("resolved $dir to dir_id: $dir_id");
+                    }
+                    $query->finish;
+		    $db->commit;
+                }
+
+                # if we found a dir_id, a row for this directory already exists
+                if (defined $dir_id) {
+                    $parent_id = $dir_id;
+                    # note that you can't exit an eval {} with next
+                    next;
+                }
+
+                # else dir doesn't exist
+                unless ($p{create}) {
+                    # resolution failed
+                    $parent_id = undef;
+                    last;
+                }
+
+                {
+                    # dir doesn't exist, create it
+                    my $query = $db->prepare_cached($sql->new_directory);
+                    $query->execute($dir, $parent_id);
+                    $query->finish;
+		    $db->commit;
+                }
+
+                # get the dir_id of the new directory entry 
+                {
+                    my $query = $db->prepare_cached($sql->last_insert_id);
+                    $query->execute();
+
+                    # the new dir_id will be the parent_id of the next
+                    # descendent directory
+                    ($parent_id) = $query->fetchrow_array;
+                    $query->finish;
+		    $db->commit;
+                }
+                die("failed to get LAST_INSERT_ID()")
+                    unless $parent_id;
+            }
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                $parent_id = 1;
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            if ($@ =~ qr/Duplicate entry/) {
+                $log->warn("Duplicate database entry, retrying transaction: $@");
+                $parent_id = 1;
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("error: $@");
+        }
+        last;
+    }
+
+    $log->debug("leaving");
+
+    return $p{dir_id} ? $dir_id : $parent_id;
+}
+
+
+sub rename_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key, $newkey) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        # make sure the "newkey" doesn't already exist
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is not valid object key' => sub { not $self->_is_valid_object_key($_[0]) },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volumes
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+    eval {
+        $newkey = parse_neb_key($newkey);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    # XXX this may require database migration in the future
+    unless ($self->_db_index_for_key($key)
+         == $self->_db_index_for_key($newkey)) {
+        $log->logdie("can not rename objects across distributed database boundaries");
+    }
+
+TRANS: while (1) {
+        eval {
+            # rename storage_object
+            my $query = $db->prepare_cached($sql->rename_object); 
+            # this SQL statment takes the new key name as the first param
+            my $rows = $query->execute($newkey->path, basename($newkey->path), $self->_resolve_dir_parent_id(key => $newkey, create => 1), $key->path);
+
+            # if we affected more then one row something very bad has happened.
+            unless ($rows == 1) {
+                $query->finish;
+                die("affected row count is $rows instead of 1");
+            }
+
+            $self->cache->delete($key->path) if defined $self->cache;
+            $self->cache->set($newkey->path, 1) if defined $self->cache;
+
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("database error: $@");
+        }
+        last;
+    } 
+
+    $log->debug("leaving");
+
+    return $newkey;
+}
+
+sub swap_objects
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key1, $key2) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volumes
+    eval {
+        $key1 = parse_neb_key($key1);
+    };
+    $log->logdie("$@") if $@;
+    eval {
+        $key2 = parse_neb_key($key2);
+    };
+    $log->logdie("$@") if $@;
+
+    my $dbidx1 = $self->_db_index_for_key($key1);
+    my $dbidx2 = $self->_db_index_for_key($key2);
+    $log->logdie("cannot swap keys not stored on the same database")
+        unless ($dbidx1 == $dbidx2);
+
+    my $dbh1 = $self->_db_for_index($dbidx1);
+    my $dbh2 = $self->_db_for_index($dbidx2);
+    $log->logdie("different db handles for the same db?")
+        unless ($dbh1 == $dbh2);
+
+    # order of operations for the swap with a single db is:
+    # key1 -> key1.swap
+    # key2 -> key1
+    # key1.swap -> key2
+
+    my $db = $dbh1;
+  TRANS: while (1) {
+        eval {
+            {
+              # key1 -> key1.swap
+              my $query = $db->prepare_cached($sql->rename_object); 
+              # this SQL statment takes the new key name as the first param
+              # XXX currently using a bogus dir_id -- this may cause a problem
+              # someday but it's unlikley as it's contained entirely in the
+              # transaction
+              my $rows = $query->execute($key1->path . ".swap", basename($key1->path) . ".swap", 1, $key1->path);
+
+              # if we affected more then one row something very bad has happened.
+              unless ($rows == 1) {
+                  $query->finish;
+                  die("affected row count is $rows instead of 1");
+              }
+          }
+
+          {
+              # key2 -> key1
+              my $query = $db->prepare_cached($sql->rename_object); 
+              # this SQL statment takes the new key name as the first param
+              my $rows = $query->execute($key1->path, basename($key1->path), $self->_resolve_dir_parent_id(key => $key1, create => 1), $key2->path);
+
+              # if we affected more then one row something very bad has happened.
+              unless ($rows == 1) {
+                  $query->finish;
+                  die("affected row count is $rows instead of 1");
+              }
+          }
+
+          {
+              # key1.swap -> key2
+              my $query = $db->prepare_cached($sql->rename_object); 
+              # this SQL statment takes the new key name as the first param
+              my $rows = $query->execute($key2->path, basename($key2->path), $self->_resolve_dir_parent_id(key => $key2, create => 1), $key1->path . ".swap");
+
+              # if we affected more then one row something very bad has happened.
+              unless ($rows == 1) {
+                  $query->finish;
+                  die("affected row count is $rows instead of 1");
+              }
+          }
+
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("database error: $@");
+        }
+      last;
+  }
+
+    $log->debug("leaving");
+
+    return 1;
+}
+
+# EAM : from JH, below is a possible option to swap instances across
+# dbs.  I recommend that this action be disallowed, and instead such
+# operations be implemented only as a combination of copy and delete
+
+# order of operations for the swap between two dbs is:
+# key1 start transaction
+# key1 -> read all instances
+# key1 -> remove all instances
+# key2 start transaction
+# key2 -> read all instances
+# key2 -> remove all instances
+# key1 -> insert key 2 instances
+# key2 -> insert key 1 instances
+# key1,2 commit
+
+sub replicate_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key, $dest_vol_name) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key'
+                    => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR|UNDEF,
+            optional    => 1,
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # if a volume name is explicity specified then we should make the
+    # replication onto that volume (even if there is already an instance on that
+    # volume) if at all possible and throw an error if we can not.
+
+    # if a volume name IS NOT specified then we should make the replication
+    # onto any (hopefully the best) volume that DOES NOT already have an
+    # instance on it.  If all available volumes already have an instance on them
+    # then we should throw an error 
+
+    # volume names implied as part of the key are *IGNORED* as the source and
+    # *SHOULD NOT* be used as the destination either
+
+    eval {
+	# EAM 2019.10.31 : if I supply dest_vol_name here, then
+	# $key->volume will point at that volume and the section below
+	# will correctly be testing the validity of dest_vol_name
+        $key = parse_neb_key($key, $dest_vol_name);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    ## Old comment:
+    # puke if the source volume is bogus, we may want to actually use this as
+    # the instance to be copied later
+
+    # EAM 2019.10.31 : in the past, the key-implied volume was
+    # supplied here, which was inconsistent since above it says the
+    # key-implied volume is ignored. By supplying $dest_vol_name to
+    # parse_neb_key above, the key->volume now refers to the
+    # dest_volume
+    if (defined $key->volume and not $self->_is_valid_volume_name($key, $key->volume)) {
+        unless ($key->hard_volume) {
+	    # reduce verbosity
+            $log->debug($key->volume . " not a known volume name");
+        } else {
+           $log->logdie("$key is not a valid volume name");
+        }
+    }
+
+    # puke if the destination volume is bogus
+    ## XXX if I supply dest_vol_name to parse_neb_key above, this section below is redundant with the above
+    ## if (defined $dest_vol_name
+    ##     and not $self->_is_valid_volume_name($key, $dest_vol_name)) {
+    ##        $log->logdie($key->volume . " is not a valid volume name");
+    ## }
+
+    $get_storage_volume_calls = 0;
+
+    my ($vol_id, $vol_host, $vol_path, $vol_xattr);
+    if (defined $dest_vol_name) {
+	# if we supplied dest_vol_name, then the call to parse_neb_key above
+	# will have set $key->volume to that value and hard_volume to match
+        ($vol_id, $vol_host, $vol_path, $vol_xattr)
+            = $self->_get_storage_volume($key, $key->volume, $key->hard_volume);
+    } else {
+        ($vol_id, $vol_host, $vol_path, $vol_xattr)
+            = $self->_get_replication_volume($key);
+    }
+
+    # print "selected target: $vol_id, $vol_host\n";
+
+    my $uri;
+TRANS: while (1) {
+        eval {
+            my $so_id;
+            {
+                # verify that at least one instance is currently available
+                my $query = $db->prepare_cached( $sql->get_object_instances );
+                my $rows = $query->execute($key->path, 1);
+
+                unless ( $rows > 0 ) {
+                    $query->finish;
+                    die( "storage object does not exist" );
+                }
+
+                $so_id = $query->fetchrow_hashref->{ 'so_id' };
+                $query->finish;
+		$db->commit;
+            }
+
+            {
+                my $query = $db->prepare_cached( $sql->new_instance );
+                $query->execute($so_id, $vol_id);
+            }
+
+            my $ins_id;
+            {
+                my $query = $db->prepare_cached( $sql->last_insert_id );
+                $query->execute();
+                ($ins_id) = $query->fetchrow_array;
+                # XXX finish seems to be required when using LAST_INSERT_ID() or we
+                # get a warning about the stmt handling still being active the next
+                # time LAST_INSERT_ID() is invoked
+                $query->finish;
+		$db->commit;
+            }
+
+            # Unfortunately, since we want to use the instance row's ID as part of
+            # the actual on disk file name we can't try to create the file until
+            # after we've create both a new storage_storage object and instance.
+
+            # TODO add some stuff here to retry if unsucessful
+            # XXX if this fails, it should try to generate the
+            # instance on another volume (unless !$soft_volume) 
+            $uri = $self->_create_empty_instance_file($key, $so_id, $ins_id, $vol_path, $vol_xattr);
+
+            {
+                my $query = $db->prepare_cached( $sql->update_instance_uri );
+                # vol_id, uri, ins_id
+                $query->execute($vol_id, $uri, $ins_id);
+            }
+
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            # handle soft volumes
+            if (defined $dest_vol_name and not defined $key->hard_volume) {
+                $log->debug("retrying with 'any' volume");
+                return $self->replicate_object($key->path, 'any');
+            }
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("error: $@");
+        }
+        last;
+    }
+
+    # check to see if the user.mode xattr exists
+    eval {
+        my $mode = $self->getxattr_object("$key", 'user.mode');
+        if (defined $mode && $mode ne "") {
+	    $self->chmod_object("$key", $mode);
+        }
+    };
+    if ($@) {
+        unless ($@ =~ qr/user.mode does not exist/) {
+            $log->logdie("error: $@");
+        }
+    }
+
+    $log->debug("leaving");
+    return "$uri";
+}
+
+
+sub prune_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key'
+                    => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    my $rows_removed = 0;
+TRANS: while (1) {
+        eval {
+            # remove key from cache
+            $self->cache->delete($key->path) if defined $self->cache;
+
+            my $so_id;
+            {
+                my $query = $db->prepare_cached( $sql->find_object_by_ext_id );
+                $query->execute( $key->path );
+                $so_id = $query->fetchrow_hashref->{'so_id'};
+                $query->finish;
+            }
+
+            # record the path of the innaccesible files for deferred
+            # deletion
+            my $rows_copied;
+            {
+                my $query = $db->prepare_cached( $sql->copy_dead_instances_to_deleted );
+                $rows_copied = $query->execute( $so_id );
+            }
+
+            # check to see if there is anything to be done
+            unless ($rows_copied > 0) {
+                $db->rollback;
+                return;
+            }
+
+            # In MySQL you can't select from a table you're deleting rows from so
+            # we first have to get a list of instances to be removed, and then
+            # remove them.
+            my $rows_found;
+            {
+                my $query = $db->prepare_cached( $sql->find_dead_instances_by_so_id );
+                $rows_found = $query->execute( $so_id );
+		my $i = 0;
+		while (my $row = $query->fetchrow_hashref) {
+                    # remove dead instances
+#		    $log->warn("copied: $rows_copied found: $rows_found removed: $rows_removed");
+                    my $query = $db->prepare_cached( $sql->delete_instance_by_ins_id);
+                    $rows_removed += $query->execute( $row->{ins_id} );
+                }
+                $query->finish;
+            }
+#	    $log->warn("copied: $rows_copied found: $rows_found removed: $rows_removed");
+            # sanity check
+            die("instances inaccessible ($rows_copied) != instances removed ($rows_removed)")
+                unless $rows_copied == $rows_removed;
+            
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("error: $@");
+        }
+        last;
+    }
+
+    $log->debug("leaving");
+
+    return $rows_removed;
+}
+
+
+sub lock_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ( $key, $type ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    my $so_id;
+    my $read_lock;
+    my $write_lock;
+
+TRANS: while (1) {
+        eval {
+            {
+                # this will set update locks
+                my $query = $db->prepare_cached( $sql->get_object_locks );
+                my $rows = $query->execute( $key->path );
+                unless ( $rows == 1 ) {
+                    $query->finish;
+                    die( "storage object does not exist" );
+                }
+
+                my $row = $query->fetchrow_hashref;
+                $query->finish;
+
+                $so_id      = $row->{ 'so_id' };
+                $read_lock  = $row->{ 'read_lock' };
+                $write_lock = $row->{ 'write_lock' };
+            }
+
+            if ($type eq 'write') {
+                # can't set a write lock twice and
+                # can't set a write lock if there are read locks
+                if ($write_lock) {
+                    die("can not write lock twice -- retry");
+                }
+                
+                if ($read_lock > 0) {
+                    die("can not write lock after read lock -- retry");
+                }
+
+                {
+                    my $query = $db->prepare_cached( $sql->set_write_lock );
+                    my $rows = $query->execute($key->path);
+                
+                    # if we affected more then one row something very bad has happened.
+                    unless ($rows == 1) {
+                        die("affected row count is $rows instead of 1");
+                    }
+
+                }
+            } elsif ($type eq 'read') {
+                # can't set a read lock if there's a write lock
+                if ($write_lock) {
+                    die("can not read lock after write lock -- retry");
+                }
+
+                {
+                    my $query = $db->prepare_cached( $sql->increment_read_lock );
+                    my $rows = $query->execute($key->path);
+                
+                    # if we affected more then one row something very bad has happened.
+                    unless ($rows == 1) {
+                        die("affected row count is $rows instead of 1");
+                    }
+                }
+            }
+
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("error: $@");
+        }
+        last;
+    }
+
+    $log->debug("leaving");
+
+    return 1;
+}
+
+
+sub unlock_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ( $key, $type ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    my $so_id;
+    my $read_lock;
+    my $write_lock;
+
+TRANS: while (1) {
+        eval {
+            {
+                # this will set update locks
+                my $query = $db->prepare_cached( $sql->get_object_locks );
+                my $rows = $query->execute($key->path);
+                unless ($rows == 1) {
+                    $query->finish;
+                    die("storage object does not exist");
+                }
+
+                my $row = $query->fetchrow_hashref;
+                $query->finish;
+
+                $so_id      = $row->{ 'so_id' };
+                $read_lock  = $row->{ 'read_lock' };
+                $write_lock = $row->{ 'write_lock' };
+            }
+
+            if ($type eq 'write') {
+                # can't remove a write lock if it doesn't exist
+                if ($read_lock) {
+                    die("can not have a write lock under a read lock");
+                }
+
+                unless ($write_lock) {
+                    die("can not remove non-existant write lock");
+                }
+
+                {
+                    my $query = $db->prepare_cached( $sql->delete_write_lock );
+                    my $rows = $query->execute($key->path);
+                
+                    # if we affected more then one row something very bad has happened.
+                    unless ($rows == 1) {
+                        die("affected row count is $rows instead of 1");
+                    }
+                }
+            } elsif ($type eq 'read') {
+                # can't remove a read lock if there's a write lock and
+                # can't remove a read lock if there aren't any
+                if ($write_lock) {
+                    die("can not have a read lock under a write lock");
+                }
+                   
+                if ($read_lock == 0) {
+                    die("can not remove non-existant read lock");
+                }
+
+                {
+                    my $query = $db->prepare_cached( $sql->decrement_read_lock );
+                    my $rows = $query->execute($key->path);
+                
+                    # if we affected more then one row something very bad has happened.
+                    unless ($rows == 1) {
+                        die("affected row count is $rows instead of 1");
+                    }
+
+                }
+            }
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("error: $@");
+        }
+        last;
+    }
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+
+sub setxattr_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key, $name, $value, $flags) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'xattr is in user. namespace'
+                    => sub { ($_[0]) =~ qr/^user\./ },
+            },
+        },
+        {
+            type        => SCALAR,
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is read or write' => sub { $_[0] =~ /^(?:create|replace)$/i },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+TRANS: while (1) {
+        eval {
+            my $query;
+
+            if ($flags eq 'create') {
+                $query = $db->prepare_cached( $sql->new_object_xattr );
+            } else {
+                # replace
+                $query = $db->prepare_cached( $sql->replace_object_xattr );
+            }
+
+            # name, value, ext_id
+            my $rows = $query->execute($name, $value, $key->path);
+            $query->finish;
+
+            # if we affected more then one row something very bad has happened.
+            if ($flags eq 'create') {
+                unless ($rows == 1) {
+                    die( "affected row count is $rows instead of 1" );
+                }
+            } else {
+                # replace_object_xattr can effect either 1 or 2 rows.  2 rows in
+                # the case of a replace and 1 if the xattr didn't already exist.
+                unless ($rows == 1 or $rows == 2) {
+                    die( "affected row count is $rows instead of 2" );
+                }
+            }
+
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("database error: $@");
+        }
+        last;
+    }
+
+    $log->debug("leaving");
+
+    return 1;
+}
+
+
+sub getxattr_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key, $name) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'xattr is in user. namespace'
+                    => sub { ($_[0]) =~ qr/^user\./ },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    my $value;
+    eval {
+        my $query = $db->prepare_cached( $sql->get_object_xattr );
+        # ext_id, name
+        my $rows = $query->execute($key->path, $name);
+
+        # no rows returned means that the xattr does not exist
+        if ($rows == 0) {
+            $query->finish;
+            die( "xattr $key:$name does not exist" );
+        }
+
+        # if we go more then one row bad something very bad has happened.
+        unless ($rows == 1) {
+            $query->finish;
+            die( "affected row count is $rows instead of 1" );
+        }
+
+        my $row = $query->fetchrow_hashref;
+        # XXX: DBI bug? ->finish is needed here even though $query is going out
+        # of scope
+        $query->finish;
+        $value = $row->{ 'value' };
+    };
+    $db->commit;
+
+    if ($@) {
+        if ($@ =~ /user\..*? does not exist/) {
+            # do not log xattr does not exist messages
+	    # NOTE: the client is using the reported message to interpret the errors
+            die $@;
+        }
+        $log->logdie("database error: $@") if $@;
+    }
+
+    $log->debug("leaving");
+
+    return $value;
+}
+
+
+sub listxattr_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    my @xattrs;
+    eval {
+        my $query = $db->prepare_cached( $sql->list_object_xattr );
+        # ext_id
+        my $rows = $query->execute($key->path);
+
+        while (my $row = $query->fetchrow_hashref) {
+            push @xattrs, $row->{ 'name' };
+        }
+    };
+    $log->logdie("database error: $@") if $@;
+    $db->commit;
+
+    $log->debug("leaving");
+
+    return \@xattrs;
+}
+
+
+sub removexattr_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key, $name) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'xattr is in user. namespace'
+                    => sub { ($_[0]) =~ qr/^user\./ },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+TRANS: while (1) {
+        eval {
+            my $query = $db->prepare_cached( $sql->remove_object_xattr );
+            # ext_id, name
+            my $rows = $query->execute($key->path, $name);
+            $query->finish;
+
+            # no rows affected means the xattr did not exist
+            if ($rows == 0) {
+                die( "xattr $key:$name does not exist" );
+            }
+
+            # if we affected more then one row something very bad has happened.
+            if ($rows > 1) {
+                die( "affected row count is $rows instead of 1" );
+            }
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("database error: $@");
+        }
+        last;
+    } 
+
+    $log->debug("leaving");
+
+    return 1;
+}
+
+# loop over all db_index values, passing db_index to each call
+sub find_objects
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($pattern) = validate_pos( @_,
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+
+    eval {
+        $pattern = parse_neb_key($pattern) if defined $pattern;
+    };
+    $log->logdie("$@") if $@;
+
+    unless (defined $pattern) {
+        $log->debug( "leaving" );
+        $log->logdie("no keys found");
+    }
+
+    my @keys = ();
+    my $n_dbs = $self->config->n_db();
+    for (my $index = 0; $index < $n_dbs; $index ++) {
+        my $newkeys = $self->_find_objects_for_index($index, $pattern);
+        push @keys, @$newkeys;
+    }
+    $log->logdie("no keys found") unless ( scalar @keys );
+
+    if (defined $self->cache) {
+    	foreach my $path (@keys) {
+            $self->cache->set($path, 1);
+        	$log->debug("key added to cache as: $path");
+	}
+    }
+
+    $log->debug( "leaving" );
+
+    return \@keys;
+}
+
+sub find_objects_wildcard
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($pattern) = validate_pos( @_,
+        {
+            type        => SCALAR,
+            optional    => 1,
+        },
+    );
+
+    my $sql = $self->sql;
+    my $db  = $self->_db_for_index(0);
+    
+    # validate that we have a key to deal with
+    eval {
+        $pattern = parse_neb_key($pattern) if defined $pattern;
+    };
+    $log->logdie("$@") if $@;
+
+    unless (defined $pattern) {
+        $log->debug( "leaving" );
+        $log->logdie("no keys found");
+    }
+
+    # parse out the directory we're working in, and decide if we are a directory
+    my $dir_id = $self->_resolve_dir_parent_id(key => $pattern, dir_id => 1);
+    my $work_dir;
+    my $file_pattern;
+    if (defined $dir_id) {
+	$work_dir = $pattern;
+	$file_pattern = '%';
+    } else {
+	my $dir_plain = dirname($pattern);
+	if ($dir_plain eq 'neb:') {
+	    $dir_plain = 'neb:///';
+	}
+	if ($dir_plain) {
+	    $work_dir = parse_neb_key($dir_plain);
+	}
+	else {
+	    $work_dir = parse_neb_key('/');
+	}
+	$file_pattern = basename $pattern;
+	unless ($file_pattern) {
+	    $file_pattern = '%';
+	}
+	
+	$dir_id = $self->_resolve_dir_parent_id(key => $work_dir, dir_id => 1);
+	unless (defined $dir_id) {
+	    # $log->logdie("pattern $work_dir does not match any key or directory");
+	    die("pattern $work_dir does not match any key or directory");
+	}
+    }
+    
+    # find dirs under dir
+    my @dir_keys;
+
+    eval {
+        $log->debug("looking for directories under dir: $dir_id");
+        my $query = $db->prepare_cached( $sql->find_dir_by_parent_id . " AND dirname LIKE ? ");
+        $query->execute( $dir_id, $file_pattern );
+	$db->commit;
+
+        while ( my $row = $query->fetchrow_hashref ) {
+            next if $row->{'dir_id'} == 1;
+            my $dir = $row->{'dirname'};
+            if ($dir_id == 1) {
+                push @dir_keys, $dir . '/' if $dir;
+            } else {
+                push @dir_keys, $work_dir->path . '/' . $dir . '/' if $dir;
+            }
+            $log->debug( "matched $dir" ) if $dir;
+        }
+    };
+    $log->logdie("database error: $@") if $@;
+
+    # find files under dir
+    my @keys;
+    eval {
+        $log->debug("looking for objects under dir: $dir_id");
+        my $query = $db->prepare_cached( $sql->find_object_by_dir_id . " AND ext_id_basename LIKE ? ");
+        $query->execute( $dir_id , $file_pattern);
+	$db->commit;
+
+        while ( my $row = $query->fetchrow_hashref ) {
+            my $key = $row->{ 'ext_id' };
+            push @keys, $key if $key;
+            $log->debug( "matched $key" ) if $key;
+        }
+    };
+    $log->logdie("database error: $@") if $@;
+
+    $log->debug( "leaving" );
+    
+    return [sort(@dir_keys), sort(@keys)];
+
+}
+
+# find matching objects from the given server
+sub _find_objects_for_index
+{
+
+    my $self    = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my $index   = shift;
+    my $key     = shift;
+
+    my $sql = $self->sql;
+    my $db  = $self->_db_for_index($index);
+
+    # first check to see if the key is an exact match
+    my @keys;
+    eval {
+        $log->debug("trying for an exact key match with $key");
+        my $query = $db->prepare_cached( $sql->find_object_by_ext_id );
+        $query->execute( $key->path );
+        if ($query->rows) {
+            my $ext_id = $query->fetchrow_hashref->{'ext_id'};
+            $log->debug( "pattern has an exact match" );
+            push @keys, $ext_id;
+        } else {
+            $log->debug("no exact match for key");
+        }
+        $query->finish;
+    };
+    if ($@) {
+        $db->rollback;
+        $log->logdie("database error: $@");
+    }
+    $db->commit;
+
+    if (scalar @keys) {
+        # it was an exact match, so stop here
+        $log->debug("leaving");
+
+        return \@keys;
+    }
+
+    # else, assume it's a directory
+    my $dir_id = $self->_resolve_dir_parent_id(key => $key, dir_id => 1);
+    unless (defined $dir_id) {
+        # $log->logdie("pattern $key does not match any key or directory");
+	# EAM : reduce verbosity
+        $log->debug("pattern $key does not match any key or directory");
+	die ("pattern $key does not match any key or directory");
+    }
+
+    # find dirs under dir
+    my @dir_keys;
+    eval {
+        $log->debug("looking for directories under dir: $dir_id");
+        my $query = $db->prepare_cached( $sql->find_dir_by_parent_id);
+        $query->execute( $dir_id );
+
+        while ( my $row = $query->fetchrow_hashref ) {
+            next if $row->{'dir_id'} == 1;
+            my $dir = $row->{'dirname'};
+            if ($dir_id == 1) {
+                push @dir_keys, $dir . '/' if $dir;
+            } else {
+                push @dir_keys, $key->path . '/' . $dir . '/' if $dir;
+            }
+            $log->debug( "matched $dir" ) if $dir;
+        }
+    };
+    $log->logdie("database error: $@") if $@;
+    $db->commit;
+
+    # find files under dir
+    eval {
+        $log->debug("looking for objects under dir: $dir_id");
+        my $query = $db->prepare_cached( $sql->find_object_by_dir_id );
+        $query->execute( $dir_id );
+
+        while ( my $row = $query->fetchrow_hashref ) {
+            my $key = $row->{ 'ext_id' };
+            push @keys, $key if $key;
+            $log->debug( "matched $key" ) if $key;
+        }
+    };
+    $log->logdie("database error: $@") if $@;
+    $db->commit;
+
+    $log->debug( "leaving" );
+    
+    return [sort(@dir_keys), sort(@keys)];
+}
+
+sub find_ext_id_by_volume
+{
+    my $self = shift;
+    my $log = $self->log;
+    $log->debug("entered - @_");
+    my ($vol_name,$limit) = validate_pos(@_,
+					{
+					    type      => SCALAR,
+# # 					    callbacks => {
+# # 						'is valid volume name' => sub {
+# # 						    return 1 if not defined $_[0];
+# # 						    $self->_is_valid_volume_name($_[0])
+# # 						},
+# 					    },
+					},
+					{
+					    type      => SCALAR|UNDEF,
+					    optional => 1,
+					},
+	);
+    unless (defined($limit)) {
+	$limit = 50000;
+    }
+
+    my $sql = $self->sql;
+    my @ext_ids;
+    my $db = $self->_db_for_index(0);
+    eval {
+	my $query;
+	$query = $db->prepare_cached( $sql->get_ext_id_by_vol_name );
+	my $rows = $query->execute($vol_name, 1,$limit);
+	unless ($rows > 0) {
+	    $query->finish;
+	    $log->logdie("no instances on storage volume or volume is not avaiable for volume: $vol_name");
+	}
+        while (my $row = $query->fetchrow_hashref) {
+            my $ext_id = $row->{ 'ext_id' };
+            push @ext_ids, $ext_id if $ext_id;
+        }
+    };
+    if ($@) {
+        $db->rollback;
+        $log->logdie("database error: $@");
+    }
+
+    # XXX remove this?
+    $log->logdie("no ext_ids found") unless (scalar @ext_ids);
+
+    $log->debug("found: \@ext_ids");
+
+    $log->debug("leaving");
+
+    return \@ext_ids;
+}
+
+# sub find_instances
+sub find_instances_old
+{
+    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,
+            optional    => 1,
+        },
+	{
+            # find_invalid
+	    type        => SCALAR|UNDEF,
+            optional    => 1,
+        }, 
+    );
+
+    my $sql = $self->sql;
+
+    # default value for find_invalid is false
+    if (not defined $find_invalid) { $find_invalid = 0; }
+
+    # vol_name overrides the key implied volume
+    eval {
+        $key = parse_neb_key($key, $vol_name);
+    };
+    $log->logdie("$@") if $@;
+    $vol_name = $key->volume;
+
+    my $db  = $self->db($key);
+
+    # 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, $key->volume)) {
+        if ($key->hard_volume) {
+            $log->logdie("$vol_name is not a valid volume name");
+        } else {
+	    # EAM : reduce verbosity
+            $log->debug( "$vol_name is not a known volume name" );
+            $vol_name = undef;
+        }
+    }
+
+    my @locations;
+    eval {
+        my $query;
+        if ($vol_name) {
+            $query = $db->prepare_cached( $sql->get_object_instances_by_vol_name );
+            # 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) and $find_invalid) {
+		# XXX returns instances which are NOT available
+		$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,
+            optional    => 1,
+        },
+	{
+            # find_invalid
+	    type        => SCALAR|UNDEF,
+            optional    => 1,
+        }, 
+    );
+    my $sql = $self->sql;
+
+    if (not defined $find_invalid) { $find_invalid = 0; }
+
+    if ($find_invalid eq "find them all") {
+	$log->logdie("old neb-cull --one_only, neb-rm --invalid both disabled by new Server.pm");
+    }
+
+    # 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 {
+	    # comment this out (too verbose)
+            # $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;
+	    if (defined($find_invalid) and $find_invalid) {
+		# returns instances which are NOT available (volume not available)
+		$rows = $query->execute($h_vol_id, $h_cab_id, $h_site_id, $key->path, 0);
+	    } else {
+		$rows = $query->execute($h_vol_id, $h_cab_id, $h_site_id, $key->path, 1);
+	    }		
+            unless ($rows > 0) {
+                $query->finish;
+                die("no instances on storage volume or volume is not available for key: $key volume: $vol_name");
+            }
+        } else {
+            $query = $db->prepare_cached( $sql->get_object_instances );
+            # ext_id, available
+
+	    my $rows;
+	    if (defined($find_invalid) and $find_invalid) {
+		# returns instances which are NOT available (volume not available)
+		$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");
+            }
+        }
+	$db->commit;
+	## if we do not call commit here, the transaction stays
+	## open blocking some operations below. 
+
+        while (my $row = $query->fetchrow_hashref) {
+            my $instance = $row->{ 'uri' };
+            push @locations, $instance if $instance;
+
+	    # Carp::carp ("instance 2: $row->{ 'uri' }, $row->{ 'vol_id' }, $row->{ 'cab_id' }, $row->{ 'vol_idx' }\n");
+        }
+    };
+    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', $find_invalid);
+        }
+        $log->logdie("database error: $@");
+    }
+
+    # XXX remove this?
+    $log->logdie("no instances found") unless (scalar @locations);
+
+    $log->debug("found: @locations");
+
+    $log->debug("leaving");
+
+    return \@locations;
+}
+
+# this method returns instances on the specified volume
+# or a list of all available instances
+# it does NOT choose instances based on proximity
+# and it does NOT return invalid instances
+sub find_instances_for_cull
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key, $vol_name) = 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,
+        },
+    );
+
+    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
+    eval {
+        $key = parse_neb_key($key, $vol_name);
+    };
+    $log->logdie("$@") if $@;
+    $vol_name = $key->volume;
+
+    my $db  = $self->db($key);
+
+    ## XXX this method is missing the alias deference section (see find_instances)
+    ## if aliases are intended to be compute nodes in the same cabinet / site as
+    ## the storage nodes, then this is not needed.
+
+    # 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, $key->volume)) {
+        if ($key->hard_volume) {
+            $log->logdie("$vol_name is not a valid volume name");
+        } else {
+	    # EAM : reduce verbosity
+            $log->debug( "$vol_name is not a known volume name" );
+            $vol_name = undef;
+        }
+    }
+
+    my @locations;
+    eval {
+        my $query;
+        if ($vol_name) {
+            $query = $db->prepare_cached( $sql->get_object_instances_by_vol_name );
+            # 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 available for key: $key volume: $vol_name");
+            }
+        } else {
+            $query = $db->prepare_cached( $sql->get_object_instances );
+            # ext_id, available
+            my $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_hash  = { uri => $row->{ 'uri' }, 
+#				   vol_id => $row->{ 'vol_id' }, 
+#				   cab_id => $row->{ 'cab_id'} };
+	    my $instance = $row->{ 'uri' };
+            push @locations, $row if $instance;
+        }
+    };
+    if ($@) {
+        $db->rollback;
+        # handle soft volumes
+	# we need to call find_intances_for_cull here so the returned structure 
+	# is the same (find_instances just returns an array of strings)
+        if (defined $vol_name and not defined $key->hard_volume and ($vol_name ne "any")) {
+            $log->debug("retrying with 'any' volume");
+            return $self->find_instances_for_cull($key->path, 'any');
+        }
+        $log->logdie("database error: $@");
+    }
+    $db->commit;
+
+    # XXX remove this?
+    $log->logdie("no instances found") unless (scalar @locations);
+
+    $log->debug("found: @locations");
+
+    $log->debug("leaving");
+
+    return \@locations;
+}
+
+
+sub delete_instance
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($key, $uri) = validate_pos( @_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type => SCALAR|SCALARREF,
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+TRANS: while (1) {
+        eval {
+            my $total;
+            my $available;
+            my $so_id;
+            my $ins_id;
+            # find so_id for key and get count of instances
+            {
+                my $query = $db->prepare_cached( $sql->get_instance_count_by_ext_id );
+                my $rows = $query->execute($key->path);
+                unless ( $rows > 0 ) {
+                    $query->finish;
+                    die( "$key has no associated instances - this should not happen" );
+                }
+
+                my $record = $query->fetchrow_hashref;
+                $so_id      = $record->{ 'so_id' };
+                $total      = $record->{ 'total' };
+                $available  = $record->{ 'available' };
+                $query->finish;
+            }
+
+            # find ins_id for uri
+            {
+                my $query = $db->prepare_cached( $sql->get_instance_by_uri );
+                my $rows = $query->execute($so_id, $uri);
+                unless ( $rows > 0) {
+                    $query->finish;
+                    die( "no instance is associated with uri" );
+                }
+
+                $ins_id = $query->fetchrow_hashref->{ 'ins_id' };
+                $query->finish;
+            }
+
+            # remove instance
+            {
+                my $query = $db->prepare_cached( $sql->delete_instance_by_ins_id );
+                my $rows = $query->execute( $ins_id );
+                $query->finish;
+                
+                # if we affected something other then one row something very
+                # bad has happened
+                unless ( $rows == 1 ) {
+                    die( "affected row count is $rows instead of 1" );
+                }
+                
+            }
+
+            # if we just deleted the last 'available' instance associated with
+            # a storage object remove it too
+            if ( $available == 1 ) {
+                # remove key from cache
+                $self->cache->delete($key->path) if defined $self->cache;
+                
+                if ($total > $available) {
+                    $self->prune_object("$key");
+                }
+                
+                # delete the storage object, remaining instances should be
+                # removed via cascading delete
+                my $query = $db->prepare_cached( $sql->delete_object );
+                my $rows = $query->execute( $so_id );
+                $query->finish;
+
+                # TODO: this will have to be changed in order to support hardlinks
+                unless ( $rows == 1 ) {
+                    die( "affected row count is $rows instead of 2" );
+                }
+            }
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ( $@ ) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie( "database error: $@" );
+        }
+        last;
+    }
+
+    $log->debug( "leaving" );
+
+    return 1;
+}
+
+
+sub stat_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ( $key ) = validate_pos( @_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    my $stat;
+    eval {
+        my $query = $db->prepare_cached( $sql->stat_object );
+        my $rows = $query->execute($key->path);
+
+        unless ($rows == 1) {
+            die("no storage object found");
+        }
+
+        $stat = $query->fetchrow_arrayref;
+        $query->finish;
+	$db->commit;
+    };
+    $log->logdie("database error: $@") if $@;
+
+    $log->debug("leaving");
+
+    return $stat;
+}
+
+# this should have a 'db_index' as an argument
+sub mounts
+{
+    # XXX: this will only pull the mounts from one db
+    # XXX: loop over db_index and generate a single unique list
+    # XXX: or report mount info for each db server
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    validate_pos(@_); 
+
+    my $sql = $self->sql;
+    my $db  = $self->_db_for_index(0); # XXX fix as above
+
+    my $stats;
+    my $query;
+    eval {
+        $query = $db->prepare_cached( $sql->get_mounted_volumes );
+        $query->execute();
+
+        # suck that table into an AoA
+        $stats = $query->fetchall_arrayref;
+
+        $query->finish;
+    };
+    $log->logdie("database error: $@") if $@;
+    $db->commit;
+
+    $log->logdie("no mounted volumes found") unless (scalar @$stats);
+
+    $log->debug("leaving");
+
+    return $stats;
+}
+
+sub chmod_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key, $mode) = validate_pos( @_, 
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key' => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+        {
+            type        => SCALAR,
+            regex       => qr/\d{3,4}/,
+            callbacks   => {
+                'is allowable mode' => sub {
+                    $_[0] == (S_IRUSR | S_IRGRP)
+                },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    # ignore volume
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    # find all instances of this object
+    my $locations;
+    eval {
+        $locations = $self->find_instances("$key");
+    };
+    $log->logdie("error: $@") if $@;
+
+    # update each instances
+    foreach my $inst (@$locations) {
+        my $path = URI->new($inst)->path;
+
+        $self->_retry(sub { chmod($mode, $path) })
+            or $log->logdie("can not chmod() $path: $!");
+
+        # XXX I'm assuming that it's OK to fsync() a filehandle that's only
+        # open for reading?  Opening as w/rw here can fail if the chmod removes
+        # write permissions.
+        my $fh;
+        $self->_retry(sub { open($fh, '<', $path) })
+            or $log->logdie("can not open() $path: $!");
+
+        # fsync(3c)
+        $self->_retry(sub { $fh->sync() })
+            or $log->logdie("can not sync() $path: $!");
+
+        $self->_retry(sub { close($fh) })
+            or $log->logdie("can not close() $path: $!");
+    }
+
+    # stick an xattr on this object with the mode
+    # XXX this would probably be better as a field in the storage_object_attr
+    # table but since we're not planning to use this for very many objects (as
+    # a %) it may not be worth adding the extra field at this time.
+    eval {
+        $self->setxattr_object("$key", 'user.mode', $mode, 'replace');
+    };
+    $log->logdie("error: $@") if $@;
+
+    $log->debug("leaving");
+
+    return $mode;
+}
+
+sub _get_storage_volume
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    no warnings qw( uninitialized );
+    $log->debug( "entered - @_" );
+    use warnings;
+
+    my ($key, $name, $hard_volume) = @_;
+    
+    # track the number of calls to this function and
+    # give up after 10 attempts
+    $get_storage_volume_calls ++;
+
+#   $log->warn("_g_s_v: key:>$key< name:>$name< hard_vol:>$hard_volume<");
+    my $sql = $self->sql;
+    my $db  = $self->db($key);
+
+    my ($vol_id, $vol_host, $vol_path, $xattr);
+    eval {
+        my $query;
+        my $rows;
+        if ( $name ) {
+            $query = $db->prepare_cached( $sql->get_storage_volume_by_name );
+            # %free, name, avaiable, allocate
+            $rows = $query->execute($max_used_space, $name, 1, 1);
+            # XXX distinguish between non-existant and unavailable
+            unless ($rows > 0) {
+                $query->finish;
+		$db->commit;
+
+                # if a volume name was specified, and is soft, and we failed to
+                # find it, fall back to any volume
+                unless ($hard_volume) {
+                    ($vol_id, $vol_host, $vol_path, $xattr) = $self->_get_storage_volume($key);
+                    return; # this just returns out of the eval not from the subroutine
+                }
+                die("storage volume: $name is not available");
+            }
+            # when matching by name we shouldn't ever match more than once
+            if ($rows > 1) {
+                $query->finish;
+		$db->commit;
+                die("affected row count is $rows instead of 1");
+            }
+        } else {
+            $query = $db->prepare_cached( $sql->get_storage_volume );
+            # %free, avaiable, allocate
+            $rows = $query->execute($max_used_space, 1, 1, $topfew_count);
+#	    $log->warn("Storage_volume: $rows $topfew_count");
+            # there has to be at least one storage volume
+            unless ($rows > 0) {
+                $query->finish;
+		$db->commit;
+		# prevent failure in the die due to undefined variables
+		my $hard_volume_str = defined $hard_volume ? $hard_volume : "undefined";
+		my $name_str = defined $name ? $name : "undefined";
+                die("no storage volume is available for key: $key volume: $name_str hard_volume: $hard_volume_str");
+            }
+        }
+
+        my $free;
+        ($vol_id, $vol_host, $vol_path, $xattr, $free) = $query->fetchrow_array;
+        $query->finish;
+    };
+    if ($@) {
+        if ($@ =~ qr/no storage volume is available/) {
+            # this should not happen unless all volumes are full
+            $log->error($@);
+            $log->debug("retrying...");
+	    if ($get_storage_volume_calls < 10) {
+		return $self->_get_storage_volume(@_);
+	    }
+	    # else
+	    $log->logdie("no available storage volume: $@");
+        } 
+        # else
+        $log->logdie("database error: $@");
+    }
+
+    $log->logdie("failed to find a suitable volume" )
+        unless defined $vol_id and defined $vol_path;
+
+    $log->debug( "leaving" );
+
+    return ($vol_id, $vol_host, $vol_path, $xattr);
+}
+
+
+sub _get_replication_volume
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    no warnings qw( uninitialized );
+    $log->debug( "entered - @_" );
+    use warnings;
+
+    my $key = shift;
+
+    my $sql = $self->sql;
+    my $db  = $self->db($key);
+
+    my ($vol_id, $vol_host, $vol_path, $xattr, $forbidden_cabinet, $forbidden_site);
+    eval {
+        my $rows;
+	
+	my $query = $db->prepare_cached( $sql->get_cabinets_for_ext_id );
+	$rows = $query->execute($key->path);
+	unless ($rows > 0) {
+	    $query->finish;
+	    die("Requested key $key does not exist (or cabinet is undefined)");
+	}
+	if ($rows == 1) {
+	    ($forbidden_cabinet, $forbidden_site) = $query->fetchrow_array;
+	    unless (defined($forbidden_cabinet)) {
+		$forbidden_cabinet = 0;
+	    }
+	    unless (defined($forbidden_site)) {
+		$forbidden_site = 0;
+	    }
+	    $query->finish;
+	} else {
+	    # if instances are spread across multiple cabinets, allow the next instance to go anywhere
+	    # NOTE: this does not prevent the new instance from going to the same volume as one of the existing instances
+	    $forbidden_cabinet = 0;
+	    $forbidden_site    = 0;
+	    $query->finish;
+	}
+	$db->commit;
+
+        $query = $db->prepare_cached( $sql->get_replication_volume_for_ext_id );
+        # ext_id, %free, available, allocate
+
+        $rows = $query->execute($key->path, $max_used_space, 1, 1, $forbidden_cabinet, $forbidden_site, $topfew_count);
+        # XXX distinguish between non-existant and unavailable
+        unless ($rows > 0) {
+            $query->finish;
+	    # 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
+        if ($rows > 1) {
+            $query->finish;
+            die("affected row count is $rows instead of 1");
+        }
+
+        my $free;
+        ($vol_id, $vol_host, $vol_path, $xattr, $free) = $query->fetchrow_array;
+        $query->finish;
+	$db->commit;
+    };
+    $log->logdie("database error: $@") if $@;
+
+    $log->logdie("failed to find a suitable volume" )
+        unless defined $vol_id and defined $vol_path;
+
+    $log->debug( "leaving" );
+
+    return ($vol_id, $vol_host, $vol_path, $xattr);
+}
+
+
+sub _is_valid_object_key
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($key) = @_;
+
+    my $sql = $self->sql;
+
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    # check cache first
+    my $cached = $self->cache->get($key->path) if defined $self->cache;
+    if (defined $cached) {
+        $log->debug( "key $key found in cache as ", $key->path );
+        $log->debug( "leaving" );
+        return 1;
+    } else {
+        $log->debug( "key $key not found in cache" );
+    }
+
+    my $db  = $self->db($key);
+
+    my $ext_id;
+    eval {
+        my $query = $db->prepare_cached( $sql->check_object_name ); 
+        $query->execute($key->path);
+        ($ext_id) = $query->fetchrow_array;
+        $query->finish;
+        $db->commit;
+	## if we do not call commit here, the transaction stays open blocking some operations below
+    };
+    if ($@) {
+        $db->rollback;
+        $log->debug("rollback");
+        $log->logdie( "database error: $@" );
+    }
+
+    if (defined $ext_id) {
+        $log->debug( "key found in db" );
+    	# add key to cache
+        $self->cache->set($key->path, 1) if defined $self->cache;
+        $log->debug( "key added to cache as ", $key->path );
+        $log->debug( "leaving" );
+        return 1;
+    } 
+
+    $log->debug( "key not found in db" );
+    $log->debug( "leaving" );
+
+    return;
+}
+
+# in the past, _is_valid_volume_name returned 'undef'.  
+# now it either returns 1 (success) or 0 (failure)
+sub _is_valid_volume_name
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($key, $vol_name) = @_;
+
+    my $sql = $self->sql;
+
+    # the volume name implied by the key is ignored.  $key is only needed to
+    # select a database connection
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+    my $volume_info = parse_neb_volume($vol_name);
+
+    my $db  = $self->db($key);
+
+    $vol_name = $volume_info->{volume};
+
+    # handle "any" volume
+    if (($vol_name eq 'any')||($vol_name eq 'any.0')) {
+        $log->debug( "found volume name $vol_name" );
+        $log->debug( "leaving" );
+        return 1;
+    }
+
+    my ($vol_id, $vol_path);
+    eval {
+        my $query = $db->prepare_cached( $sql->get_volume_by_name ); 
+        $query->execute( $vol_name );
+        my $free;
+        ($vol_id, $vol_path, $free) = $query->fetchrow_array;
+        $query->finish;
+        $db->commit;
+    };
+    $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;
+    } 
+
+    $log->debug( "volume name $vol_name not found" );
+    $log->debug( "leaving" );
+
+    return 0;
+}
+
+
+sub _create_empty_instance_file
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($key, $so_id, $ins_id, $vol_path, $xattr) =  @_;
+
+    my $sql = $self->sql;
+    my $db  = $self->db($key);
+
+    my $uri;
+    eval {
+        my $storage_path = $self->_generate_storage_path($key->path, $vol_path);
+        my $storage_filename = $self->_generate_storage_filename($key->path, $ins_id);
+        unless (-d $storage_path) {
+            $self->_retry(sub { mkpath([$storage_path], 0, 0775) })
+                or die("can't create storage path: $storage_path");
+        }
+        # check to make sure at least the parent directory has the proper
+        # permissions
+        my $mode = [$self->_retry(sub { stat($storage_path) } )]->[2] & 07777;
+        unless ($mode == 0775) {
+            # XXX: this problem is so common that it's flooding the logs
+            $log->debug("$storage_path has the wrong permissions of: 0", sprintf("%o", $mode));
+            $self->_retry(sub { chmod(0775, $storage_path) })
+                or die("can not chmod() $storage_path: $!");
+        }
+
+        my $fqpn = File::Spec->catfile($storage_path, $storage_filename);
+        $uri = URI::file->new($fqpn);
+        $log->debug("generated uri $uri");
+        $self->_create_empty_file($uri->file);
+    };
+    if ($@) {
+        if (defined $uri and -e $uri->file) {
+            unlink($uri->file)
+                or $log->error("failed to unlink() $uri: $!");
+        }
+        $log->logdie($@);
+    }
+
+#     if ($xattr) {
+#         my $path = $uri->file;
+#         $log->logdie("can not set xattr on $path: $!")
+#             unless (setfattr($path, 'user.nebulous_key', $key->path));
+#     }
+
+    return $uri;
+}
+
+
+sub _create_empty_file
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my ($path) = @_;
+
+    # perl's open() can't do an O_CREAT | O_EXCL
+    # XXX is it possible to tell if this system call failed?
+    -e $path
+        and $log->logdie("file $path already exists");
+
+    my $fh;
+    $self->_retry(sub { open($fh, '>', $path) })
+        or $log->logdie("can not open() $path: $!");
+
+    # chmod before fsync() to make sure the changed perms hit the disk too
+    $self->_retry(sub { chmod(0664, $path) })
+        or $log->logdie("can not chmod() $path: $!");
+
+    $self->_retry(sub { $fh->sync() })
+        or $log->logdie("can not sync() $path: $!");
+
+    $self->_retry(sub { close($fh) })
+        or $log->logdie("can not close() $path: $!");
+
+    return $path;
+}
+
+
+sub _generate_storage_filename
+{
+    my $self = shift;
+
+    my ($key, $ins_id) = @_;
+
+    my $filename = $key;
+    # mangle '/'s into ':'
+    $filename =~ s|/|:|g;
+
+    return "$ins_id.$filename"
+}
+
+
+sub _generate_storage_path
+{
+    my $self = shift;
+
+    my ($key, $vol_path) = @_;
+
+    # taken and modified from Cache::File::cache_file_path()
+    # Copyright (C) 2003-2006 Chris Leishman.  All Rights Reserved.
+    my $shakey = sha1_hex($key);
+    my (@path) = unpack('A2' x SUBPATH_DEPTH, $shakey);
+
+    return File::Spec->catdir($vol_path, @path);
+}
+
+
+sub _retry
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug( "entered - @_" );
+
+    my $func = shift;
+
+    my @ret;
+    for (my $i = 0; $i < NFS_RETRIES; $i++) {
+        eval {
+            @ret = $func->(@_);
+        };
+        if ($@) {
+            $log->logdie($@);
+            sleep NFS_RETRY_WAIT;
+            next;
+        }
+
+        last;
+    }
+
+    # if the loop ended and $@ is set, rethrow the error
+    if ($@) {
+        $log->logdie($@);
+    }
+
+    return @ret;
+}
+
+
+sub DESTROY
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    my $sql = $self->sql;
+#    my $db  = $self->db;
+
+    $log->debug( "entered" );
+
+# XXX do we need to loop over db_index?
+#    $self->db->disconnect;        
+
+#    $log->debug( "disconnected from database: ", sub { $db->data_sources; } );
+
+    $log->debug( "leaving" );
+}
+
+
+1;
+
+__END__
Index: /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server.pod
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server.pod	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server.pod	(revision 42864)
@@ -0,0 +1,263 @@
+=pod
+
+=head1 NAME
+
+Nebulous::Server - Nebulous server component
+
+=head1 SYNOPSIS
+
+    use Nebulous::Server;
+
+    Nebulous::Server->new( $dsn, $username, $passwd );
+    Nebulous::Server->db( $db );
+    Nebulous::Server->log( $log );
+    Nebulous::Server->sql( $sql );
+    Nebulous::Server->config( $config );
+    Nebulous::Server->create_object( $key, $volume );
+    Nebulous::Server->rename_object( $key, $newkey );
+    Nebulous::Server->swap_objects( $key1, $key2 );
+    Nebulous::Server->replicate_object( $key, $volume );
+    Nebulous::Server->lock_object( $key, $type );
+    Nebulous::Server->unlock_object( $key, $type );
+    Nebulous::Server->setxattr_object( $key, $name, $value, $flags );
+    Nebulous::Server->getxattr_object( $key, $name );
+    Nebulous::Server->listxattr_object( $key );
+    Nebulous::Server->removexattr_object( $key, $name );
+    Nebulous::Server->find_objects( $pattern );
+    Nebulous::Server->find_instances( $key, $volume );
+    Nebulous::Server->delete_instance( $key, $uri );
+    Nebulous::Server->stat_object( $key );
+    Nebulous::Server->mounts();
+    Nebulous::Server->chmod_object( $key, $mode);
+    Nebulous::Server->prune_object( $key );
+
+=head1 DESCRIPTION
+
+This is the server side component of the Nebulous system.  It is primarily a
+middle-ware abstraction of an SQL database.
+
+=head1 USAGE
+
+Typically, this module would not be directly interacted with (although, it is
+possible to do so).  It is intended that one or more intermediate transport
+layers are used (SOAP, XML-RPC, etc.)
+
+=head2 Import Parameters
+
+This module accepts no arguments to it's C<import> method and exports no
+I<symbols>.
+
+=head2 Methods
+
+=head3 Class Methods
+
+=over 4
+
+=item * setup( $dsn, $username, $passwd );
+
+Accepts 3 parameters, all are mandatory.  Returns Boolean true.  Throws an
+exception on error.
+
+=item * create_object( $key, $volume );
+
+Accepts 4 parameters, only the first is mandatory.  Returns a URI.  Throws an
+exception on error.
+
+=item * replicate_object( $key, $volume );
+
+Accepts 2 parameters, only the first is mandatory.  Returns a URI.  Throws an
+exception on error.
+
+=item * lock_object( $key, $type );
+
+Accepts 2 parameters, all are mandatory.  Returns Boolean true.   Throws an
+exception on error.
+
+=item * unlock_object( $key, $type );
+
+Accepts 2 parameters, all are mandatory.  Returns Boolean true.   Throws an
+exception on error.
+
+=item * setxattr_object( $key, $name, $value, $flags );
+
+Accepts 4 parameters, all are mandatory.  
+
+=over 4
+
+=item C<$key>
+
+Nebulous key to create the xattr on.
+
+=item C<$name>
+
+The xattr's name.
+
+=item C<$value>
+
+The xattr's value.
+
+=item C<$flags>
+
+The xattr key insertion mode flag.  Acceptable value are either C<"create"> or
+C<"replace">.  If the xattr already exists then trying to insert another xattr
+with the same name in C<"create"> mode will fail.
+
+=back
+
+Returns Boolean true on success.  Throws an exception on error.
+
+=item * getxattr_object( $key, $name );
+
+Accepts 2 parameters, all are mandatory.  
+
+=over 4
+
+=item C<$key>
+
+Nebulous key to fetch the xattr from.
+
+=item C<$name>
+
+The xattr's name.
+
+=back
+
+Returns the string value of C<$name> on success.  Throws an exception on error.
+
+=item * listxattr_object( $key );
+
+Accepts 1 mandatory parameter. 
+
+=over 4
+
+=item C<$key>
+
+List all xattrs on this Nebulous key.
+
+=back
+
+Returns an C<ARRAYREF> on success.  Throws an exception on error.
+
+=item * removexattr_object( $key, $name );
+
+Accepts 2 parameters, all are mandatory.  
+
+=over 4
+
+=item C<$key>
+
+Nebulous key to remove the xattr from.
+
+=item C<$name>
+
+The xattr's name.
+
+=back
+
+Returns Boolean true on success.  Throws an exception on error.
+
+=item * find_instance( $key, $volume );
+
+Accepts 2 parameters, only the first is mandatory.  Returns an arrayref.
+Throws an exception on error.
+
+=item * delete_instance( $key, $uri );
+
+Accepts 2 parameters, both mandatory.  Returns Boolean true.  C<<$uri>> must be
+an instance of C<<$key>>.  Throws an exception on error.
+
+=item * swap_objects( $key1, $key2 );
+
+Accepts 2 parameters, all mandatory.  Atomically swaps the object names of
+C<$key1> and C<$key2>.  Both objects must already exist.  Return true on
+success or an exception on error.
+
+=item * stat_object( $key );
+
+Accepts 1 parameters, it is mandatory.  Returns a list of:
+
+    so_id
+    ext_id
+    read_lock
+    write_lock
+    epoch
+    mtime
+    number of available instances
+    total number of instances
+
+=item * mounts( $key );
+
+Accepts no parameters and returns an AoA of:
+
+    [
+        [
+            mountpoint,
+            total,
+            used,
+            vol_id,
+            name,
+            path,
+            allocate,
+            available,
+            xattr,
+        ],
+        [ ... ],
+        ...
+    ]
+
+=item * chmod_object( $key, $mode );
+
+Accepts 2 parameters, both mandatory.  C<$key> is the nebulous key to operate
+on and C<$mode> are the POSIX like file permissions to set on all instances of
+C<$key>.  C<$mode> must be specified in oct.  Returns C<$mode> or sucess or an
+exception on failure.
+
+=item * prune_object( $key );
+
+Removes all of the inaccessible instances from an object.
+
+Accepts 1 mandatory parameter.  Returns the number of inaccessible instances actually pruned from the object.
+
+=back
+
+=head1 DEVELOPER NOTES
+
+=head2 REFERENCES
+
+=head1 CREDITS
+
+Just me, myself, and I.
+
+=head1 SUPPORT
+
+Please contact the author directly via e-mail.
+
+=head1 AUTHOR
+
+Joshua Hoblitt <jhoblitt@cpan.org>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2004-2009  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 LICENSE file included with
+this module, or in the L<perlgpl> Pod as supplied with Perl 5.8.1 and later.
+
+=head1 SEE ALSO
+
+L<Nebulous::Client>
+
+=cut
Index: /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Apache.pm
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Apache.pm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Apache.pm	(revision 42864)
@@ -0,0 +1,25 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id
+
+package Nebulous::Server::Apache;
+
+use strict;
+use warnings FATAL => qw( all );
+
+our $VERSION = 0.01;
+
+use Nebulous::Server::SOAP;
+use SOAP::Transport::HTTP;
+
+my $server = SOAP::Transport::HTTP::Apache
+    ->dispatch_to( 'Nebulous::Server::SOAP' )
+    ->options({ compress_threshold => 10 * 1024 });
+
+no warnings qw( redefine );
+sub handler { $server->handler(@_); }
+use warnings;
+
+1;
+
+__END__
Index: /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Config.pm
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Config.pm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Config.pm	(revision 42864)
@@ -0,0 +1,139 @@
+# Copyright (C) 2005  Joshua Hoblitt
+#
+# $Id: Config.pm,v 1.5 2008-12-14 22:54:25 eugene Exp $
+
+package Nebulous::Server::Config;
+
+use strict;
+use warnings FATAL => qw( all );
+
+our $VERSION = 0.03;
+
+use base qw( Class::Accessor::Fast );
+
+use Log::Log4perl qw( :levels );
+use Params::Validate qw( validate validate_pos SCALAR ARRAYREF UNDEF );
+
+our %LEVELS = (
+    off     => $OFF,
+    fatal   => $FATAL,
+    error   => $ERROR,
+    warn    => $WARN,
+    info    => $INFO,
+    debug   => $DEBUG,
+    all     => $ALL,
+);
+
+my $db_validate = {
+    dbindex       => {
+        type => SCALAR,
+        regex => qr/^\d+$/,
+    },
+    dsn         => { type => SCALAR },
+    dbuser      => { type => SCALAR },
+    dbpasswd    => { type => SCALAR | UNDEF },
+};
+
+my $new_validate = {
+    trace       => {
+        type        => SCALAR,
+        optional    => 1,
+        default     => 'fatal',
+        callbacks   => {
+            'is valid level' => sub {
+                defined $LEVELS{ lc $_[0] };
+            },
+        },
+    },
+    dsn                 => { type => SCALAR, optional => 1 },
+    dbuser              => { type => SCALAR, optional => 1 },
+    dbpasswd            => { type => SCALAR | UNDEF, optional => 1 },
+    memcached_servers   => { type => ARRAYREF, optional => 1 },
+};
+
+__PACKAGE__->mk_ro_accessors( keys %$new_validate );
+
+
+sub new
+{
+    my $class = shift;
+
+    my %p = validate( @_, $new_validate );
+
+    # normalize log levels to lower-case
+    my $self = {
+        trace             => $LEVELS{lc($p{trace})},
+        memcached_servers => $p{memcached_servers},
+    };
+
+    bless $self, $class || ref $class;
+
+    my @dbs;
+    $self->{dbs} = \@dbs;
+
+    if (defined $p{dsn} or defined $p{dbuser} or defined $p{dbpasswd}) {
+        $self->add_db(
+            dbindex     => 0,
+            dsn         => $p{dsn},
+            dbuser      => $p{dbuser},
+            dbpasswd    => $p{dbpasswd},
+        );
+    }
+
+    return $self;
+}
+
+
+sub add_db
+{
+    my $self = shift;
+    
+    my %p = validate( @_, $db_validate );
+
+    my $config_db = Nebulous::Server::Config::DB->new({
+        dsn         => $p{dsn},
+        dbuser      => $p{dbuser},
+        dbpasswd    => $p{dbpasswd},
+    });
+
+    $self->{dbs}->[$p{dbindex}] = $config_db;
+
+    return $self;
+}
+
+
+sub db 
+{
+    my $self = shift;
+
+    my ($db_index) = validate_pos( @_, { type => SCALAR, optional => 1, });
+
+    # default to 0
+    $db_index ||= 0;
+
+    return $self->{dbs}->[$db_index];
+}
+
+
+sub n_db 
+{
+    my $self = shift;
+
+    return scalar @{ $self->{dbs} };
+}
+
+
+package Nebulous::Server::Config::DB;
+
+use strict;
+use warnings FATAL => qw( all );
+
+our $VERSION = 0.01;
+
+use base qw( Class::Accessor::Fast );
+
+__PACKAGE__->mk_ro_accessors(qw( dsn dbuser dbpasswd )); 
+
+1;
+
+__END__
Index: /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Log.pm
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Log.pm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/Log.pm	(revision 42864)
@@ -0,0 +1,62 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: Log.pm,v 1.9 2008-12-14 22:54:25 eugene Exp $
+
+package Nebulous::Server::Log;
+
+use strict;
+use warnings FATAL => qw( all );
+
+our $VERSION = '0.02';
+
+use Log::Log4perl;
+use Nebulous::Server::Config;
+
+sub init {
+    my ($self, $config) = @_;
+
+#    my $dsn         = $config->db->dsn;
+#    my $dbuser      = $config->db->dbuser;
+#    my $dbpasswd    = $config->db->dbpasswd;
+
+    my $conf = <<END;
+    log4perl.category.Nebulous.Server = WARN, SERVERLOGFILE
+
+    log4perl.appender.SERVERLOGFILE           = Log::Log4perl::Appender::File
+    log4perl.appender.SERVERLOGFILE.filename  = /tmp/nebulous_server.log
+    log4perl.appender.SERVERLOGFILE.mode      = append
+    log4perl.appender.SERVERLOGFILE.layout    = Log::Log4perl::Layout::PatternLayout
+    log4perl.appender.SERVERLOGFILE.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} | %H | %p | %M - %m%n
+#   date | hostname | priority | method/sub - message\n
+
+#    log4perl.appender.SQLLOG            = Log::Log4perl::Appender::DBI
+#    log4perl.appender.SQLLOG.sql        = \
+#    log4perl.appender.SQLLOG.datasource = 
+#    log4perl.appender.SQLLOG.username   = 
+#    log4perl.appender.SQLLOG.password   = 
+    INSERT INTO log (timestamp, hostname, level, sub, message) \
+    VALUES          (%d,        %H,       %p,    %M,  %m)
+#        VALUES          (?,         ?,        ?,     ?,   ?)
+#        log4perl.appender.SQLLOG.params.1   = %d
+#        log4perl.appender.SQLLOG.params.2   = %H
+#        log4perl.appender.SQLLOG.params.3   = %p
+#        log4perl.appender.SQLLOG.params.4   = %M
+#        log4perl.appender.SQLLOG.params.5   = %m
+#        log4perl.appender.SQLLOG.usePreparedStmt = 1
+    log4perl.appender.SQLLOG.bufferSize = 2
+    log4perl.appender.SQLLOG.layout     = Log::Log4perl::Layout::NoopLayout
+    log4perl.appender.SQLLOG.warp_message = 0
+END
+
+    if ($INC{'Apache/DBI.pm'} && $ENV{MOD_PERL}) {
+        Log::Log4perl::init_once( \$conf );
+    } else {
+        Log::Log4perl::init( \$conf );
+    }
+
+    return 1;
+}
+
+1;
+
+__END__
Index: /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/SOAP.pm
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/SOAP.pm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/SOAP.pm	(revision 42864)
@@ -0,0 +1,85 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: SOAP.pm,v 1.4.32.1 2008-12-14 22:52:37 eugene Exp $
+
+package Nebulous::Server::SOAP;
+
+use strict;
+use warnings FATAL => qw( all );
+
+our $VERSION = '0.02';
+
+import SOAP::Data 'name'; 
+use Apache2::Const -compile => qw(OK);
+use Nebulous::Server;
+use SOAP::Lite;
+
+our $AUTOLOAD;
+
+our $config;
+our $neb;
+
+
+sub new_on_init
+{
+    my $self = shift;
+
+    require mod_perl2;
+    require Apache2::Module;
+    require Apache2::ServerUtil;
+
+    $config = shift;
+
+    my $s = Apache2::ServerUtil->server;
+    $s->push_handlers(PerlChildInitHandler => \&init);
+
+    return $self;
+}
+
+
+sub init
+{
+    my $self = shift;
+
+    $neb = Nebulous::Server->new_from_config($config);        
+
+    return Apache2::Const::OK;
+}
+
+
+sub AUTOLOAD
+{
+    my $self = shift;
+
+    my ( $package, $method ) = $AUTOLOAD =~ m/(?:(.+)::)([^:]+)$/;
+    return undef if $method eq 'DESTROY';
+
+    init() unless defined $neb;
+
+    die "process $$ has not been initialized"
+        unless defined $neb;
+
+    die "method $method does not exist in Nebulous::Server"
+        unless defined ${Nebulous::Server::}{$method};
+
+    # create a sub and install it in the package so successive calls to the
+    # same method don't have to go through AUTOLOAD
+    my $method_sub;
+    eval q|
+    $method_sub = sub {
+        my $self = shift;
+        return name( result => $neb->| . $method . q|( @_ ) );
+    }
+    |;
+    die $@ if $@;
+
+    no strict 'refs';
+    *{"${package}::${method}"} = $method_sub;
+    use strict;
+
+    # invoke the method we just created
+    return $self->$method(@_);
+}
+
+
+1;
Index: /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/SQL.pm
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/SQL.pm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Nebulous/Server/SQL.pm	(revision 42864)
@@ -0,0 +1,862 @@
+# Copyright (c) 2004  Joshua Hoblitt
+#
+# $Id: SQL.pm,v 1.78 2008-12-14 22:54:25 eugene Exp $
+
+package Nebulous::Server::SQL;
+
+use strict;
+use warnings FATAL => qw( all );
+
+our $VERSION = '0.04';
+
+use base qw( Class::Accessor::Fast );
+
+my %sql = (
+    set_transaction_model   => qq{
+        SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
+    },
+    update_instance_uri => qq{
+        UPDATE instance
+        SET vol_id = ?, uri = ?
+        WHERE ins_id = ?
+    },
+    last_insert_id      => qq{
+        SELECT LAST_INSERT_ID()
+    },
+    new_object          => qq{
+        INSERT INTO storage_object
+        (so_id, ext_id, ext_id_basename, type, dir_id)
+        VALUES (?, ?, ?, 'REG_FILE', ?)
+    },
+    new_object_attr  => qq{
+        INSERT INTO storage_object_attr
+        (so_id, read_lock, write_lock)
+        VALUES (?, 0, NULL)
+    },
+    delete_object       => qq{
+        DELETE FROM storage_object
+        WHERE storage_object.so_id = ?
+    },
+    new_object_instance => qq{
+        INSERT INTO instance 
+        (so_id, vol_id, uri)
+        VALUES (LAST_INSERT_ID(), ?, 'error')
+    },
+    new_instance        => qq{
+        INSERT INTO instance
+        (so_id, vol_id, uri)
+        VALUES (?, ?, 'error')
+    },
+    get_object          => qq{
+        SELECT
+            so_id,
+            ext_id,
+            read_lock,
+            write_lock,
+            epoch,
+            mtime
+        FROM storage_object
+        JOIN storage_object_attr
+        USING (so_id)
+        WHERE ext_id = ?
+    },
+    get_directory       => qq{
+        SELECT
+            dir_id
+        FROM directory
+        WHERE parent_id = ?
+            AND dirname = ?
+        LOCK IN SHARE MODE
+    },
+    new_directory       => qq{
+        INSERT INTO directory
+        (dirname, parent_id)
+        VALUES (?, ?)
+    },
+    check_object_name => qq{
+        SELECT
+            so_id,
+            ext_id
+        FROM storage_object
+        WHERE ext_id = ?
+    },
+    stat_object          => qq{
+        SELECT
+            so.so_id,
+            so.ext_id,
+            attr.read_lock,
+            attr.write_lock,
+            attr.epoch,
+            attr.mtime,
+            SUM(available) as available,
+            COUNT(instance.so_id) as instances
+        FROM storage_object as so
+        JOIN storage_object_attr as attr
+            USING (so_id)
+        JOIN instance
+            USING (so_id)
+        LEFT JOIN mountedvol
+            USING(vol_id)
+        WHERE ext_id = ?
+        GROUP BY so.so_id
+    },
+    # Note: this sets an update lock
+    get_object_locks    => qq{
+        SELECT
+            storage_object.so_id,
+            read_lock,
+            write_lock 
+        FROM storage_object
+        JOIN storage_object_attr
+        USING (so_id)
+        WHERE ext_id = ?
+        FOR UPDATE
+    },
+    new_object_xattr  => qq{
+        INSERT INTO storage_object_xattr
+            SELECT
+                so_id,
+                ?,
+                ?        
+            FROM storage_object
+            WHERE ext_id = ?
+    },
+    replace_object_xattr  => qq{
+        REPLACE INTO storage_object_xattr
+            SELECT
+                so_id,
+                ?,
+                ?        
+            FROM storage_object
+            WHERE ext_id = ?
+    },
+    list_object_xattr    => qq{
+        SELECT storage_object_xattr.name
+        FROM storage_object
+        JOIN storage_object_xattr
+        USING (so_id)
+        WHERE ext_id = ?
+    },
+    get_object_xattr    => qq{
+        SELECT storage_object_xattr.value
+        FROM storage_object
+        JOIN storage_object_xattr
+        USING (so_id)
+        WHERE ext_id = ?
+            AND name = ?
+    },
+    remove_object_xattr    => qq{
+        DELETE FROM storage_object_xattr
+        WHERE so_id = (SELECT so_id from storage_object where ext_id = ?)
+            AND name = ?
+    },
+    set_write_lock      => qq{
+        UPDATE storage_object_attr
+        JOIN storage_object
+        USING(so_id)
+        SET write_lock = 'write'
+        WHERE storage_object.ext_id = ?
+    },
+    delete_write_lock   => qq{
+        UPDATE storage_object_attr
+        JOIN storage_object
+        USING(so_id)
+        SET write_lock = NULL
+        WHERE ext_id = ?
+    },
+    increment_read_lock => qq{
+        UPDATE storage_object_attr
+        JOIN storage_object
+        USING(so_id)
+        SET read_lock = read_lock + 1
+        WHERE ext_id = ?
+    },
+    decrement_read_lock => qq{
+        UPDATE storage_object_attr
+        JOIN storage_object
+        USING(so_id)
+        SET read_lock = read_lock - 1
+        WHERE ext_id = ?
+    },
+    delete_instance_by_ins_id => qq{
+        DELETE FROM instance
+        WHERE ins_id = ?
+    },
+    get_object_from_uri   => qq{
+        SELECT so_id
+        FROM instance
+        WHERE uri = ?
+    },
+    get_instance_by_uri   => qq{
+        SELECT ins_id
+        FROM instance
+        WHERE
+            so_id = ?
+            AND uri = ?
+    },
+    get_instance_count   => qq{
+        SELECT count(ins_id)
+        FROM instance
+        WHERE so_id = ?
+    },
+    get_instance_count_by_ext_id   => qq{
+        SELECT 
+            so_id,
+            count(ins_id) as total,
+            sum(available) as available
+        FROM instance
+        JOIN storage_object
+            USING(so_id)
+        LEFT JOIN mountedvol
+            USING(vol_id)
+        WHERE
+            ext_id = ?
+        GROUP BY so_id
+    },
+    copy_dead_instances_to_deleted => qq{
+        INSERT INTO deleted 
+        SELECT vol_id, uri, NULL
+        FROM instance
+        LEFT JOIN mountedvol
+            USING(vol_id)
+        WHERE
+            so_id = ?
+            AND (mountedvol.available IS NULL || mountedvol.available = 0)
+    },
+    find_dead_instances_by_so_id => qq{
+        SELECT so_id, ins_id, vol_id, uri
+        FROM instance
+        LEFT JOIN mountedvol
+            USING(vol_id)
+        WHERE
+            so_id = ?
+            AND (mountedvol.available IS NULL || mountedvol.available = 0)
+    },
+    get_object_instances    => qq{
+        SELECT
+            storage_object.so_id,
+            uri,
+            mountedvol.available,
+            vol_id,
+            cab_id
+        FROM storage_object
+        JOIN instance
+            USING (so_id)
+        JOIN mountedvol
+            USING(vol_id)
+        JOIN volume
+            USING(vol_id)
+        WHERE ext_id = ?
+            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
+            storage_object.so_id,
+            uri,
+            vol_id,
+            cab_id
+        FROM storage_object
+        JOIN instance
+            USING (so_id)
+        JOIN mountedvol
+            USING(vol_id)
+        JOIN volume
+            USING(vol_id)
+        WHERE ext_id = ?
+            AND mountedvol.name = ?
+            AND mountedvol.available = ?
+    },
+    get_ext_id_by_vol_name => qq{
+        SELECT
+            ext_id
+        FROM instance
+        JOIN storage_object
+            USING (so_id)
+        JOIN mountedvol
+            USING(vol_id)
+        JOIN volume
+            USING(vol_id)
+        WHERE volume.name = ?
+            AND mountedvol.available = ?
+        LIMIT ?
+    },
+    # volume handler
+    get_storage_volume_by_name   => qq{
+        SELECT
+            vol_id,
+            host,
+            path,
+            xattr,
+            total - used as free
+        FROM mountedvol
+        WHERE
+            used / total < ?
+            AND name = ?
+            AND available = ?
+            AND allocate = ?
+        ORDER BY free DESC
+        LIMIT 1
+    },
+    # volume handler
+    get_cabinets_for_ext_id            => qq{
+        SELECT DISTINCT 
+            volume.cab_id, site_id
+        FROM instance 
+        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 = ?
+    },
+    get_replication_volume_for_ext_id    => qq{
+        SELECT * FROM (
+        SELECT
+            m.vol_id,
+            m.host,
+            m.path,
+            m.xattr,
+            total - used as free
+        FROM mountedvol AS m
+        JOIN volume AS v USING(vol_id)
+        JOIN cabinet AS c USING(cab_id)
+        LEFT JOIN (
+                   SELECT
+                       instance.vol_id,
+                       so_id
+                   FROM instance
+                   JOIN volume 
+                   ON instance.vol_id = volume.vol_id
+		   WHERE so_id = (
+				  SELECT so_id
+				  FROM storage_object
+				  WHERE ext_id = ?
+				 )
+                  ) AS i
+            ON m.vol_id = i.vol_id
+         WHERE
+             i.vol_id IS NULL
+             AND used / total < ?
+             AND m.available = ?
+             AND m.allocate = ?
+--             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
+         ORDER BY RAND()
+         LIMIT 1
+    },
+    # volume handler
+    # This has a hack to get around the lack of location awareness by using xattr.
+    get_storage_volume          => qq{
+        SELECT * from (
+        SELECT
+            vol_id,
+            host,
+            path,
+            xattr,
+            total - used as free
+        FROM mountedvol
+        WHERE
+            used / total < ?
+            AND available = ?
+            AND allocate = ?
+            AND xattr = 0
+        ORDER BY free DESC
+        LIMIT ?) as topfew
+        ORDER BY RAND()
+        LIMIT 1
+    },
+    new_cabinet         => qq{
+        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{
+        SELECT vol_id, name, host, path
+        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{
+        SELECT
+            v.vol_id,
+            v.name,
+            v.host,
+            v.path,
+            v.allocate,
+            v.available,
+            v.xattr,
+            mountedvol.vol_id IS NOT NULL as mounted,
+            v.cab_id,
+            v.last_modified,
+            v.note
+        FROM volume AS v
+        LEFT JOIN mountedvol
+            USING(vol_id)
+    },
+    find_object_by_ext_id => qq{
+        SELECT so_id, ext_id, ext_id_basename
+        FROM storage_object
+        WHERE ext_id = ?
+    },
+    find_object_by_dir_id => qq{
+        SELECT ext_id, ext_id_basename
+        FROM storage_object
+        WHERE dir_id = ?
+    },
+    find_dir_by_parent_id => qq{
+        SELECT dir_id, dirname
+        FROM directory
+        WHERE parent_id = ?
+    },
+    rename_object => qq{
+        UPDATE storage_object
+        SET ext_id = ?, ext_id_basename = ?, dir_id = ?
+        WHERE ext_id = ?
+    },
+    find_objects_with_unavailable_instances => qq{
+        SELECT
+            storage_object.so_id,
+            ext_id,
+            count(ins_id) as instances,
+            volume.name as volume_name,
+            volume.host as volume_host,
+            count(mymountedvol.vol_id) as available_instances,
+            count(mymountedvol.vol_id) > 0 as recoverable,
+            storage_object_xattr.value as copies
+        FROM storage_object
+        JOIN instance
+            USING(so_id)
+        JOIN volume
+            USING(vol_id)
+        LEFT JOIN storage_object_xattr
+            ON storage_object.so_id = storage_object_xattr.so_id
+        JOIN mymountedvol
+            USING(vol_id)
+        WHERE mymountedvol.available = 1
+--        WHERE storage_object_xattr.name = 'user.copies'
+        GROUP BY so_id
+        HAVING available_instances < instances OR instances < copies
+    },
+    find_objects_with_extra_instances_by_xattr => qq{
+        SELECT
+            so.so_id,
+            so.ext_id,
+            count(ins_id) as instances,
+            mv.name as volume_name,
+            mv.host as volume_host,
+            count(mv.vol_id) as available_instances,
+            xattr.value as copies
+        FROM storage_object AS so
+        JOIN storage_object_xattr as xattr
+            ON so.so_id = xattr.so_id
+            AND xattr.name = 'user.copies'
+        JOIN instance AS i
+            ON so.so_id = i.so_id
+        JOIN mountedvol AS mv
+            USING(vol_id)
+        WHERE
+            mv.available = 1
+        GROUP BY so_id
+        HAVING available_instances > copies
+        limit 5;
+    },
+    find_objects_with_extra_instances => qq{
+        SELECT
+            storage_object.so_id,
+            ext_id,
+            count(ins_id) as instances,
+            volume.name as volume_name,
+            volume.host as volume_host,
+            count(mymountedvol.vol_id) as available_instances,
+            count(mymountedvol.vol_id) > 0 as recoverable,
+            storage_object_xattr.value as copies
+        FROM storage_object
+        JOIN instance
+            USING(so_id)
+        JOIN volume
+            USING(vol_id)
+        LEFT JOIN storage_object_xattr
+            ON storage_object.so_id = storage_object_xattr.so_id
+        JOIN mymountedvol
+            USING(vol_id)
+        WHERE
+            mymountedvol.available = 1
+            AND storage_object_xattr.name = 'user.copies'
+        GROUP BY so_id
+        HAVING available_instances > copies
+    },
+    get_mounted_volumes => qq{
+        SELECT mountpoint, total, used, vol_id, name, host, path, allocate, available, xattr FROM mountedvol ORDER BY host, name
+    },
+);
+
+{
+    my @schema;
+
+    local $/ = '###';
+
+    foreach my $statement (<DATA>) {
+        last unless ( $statement =~ /\S+/ );
+        $statement =~ s/###//g;
+        push @schema, $statement;
+    }
+
+    $sql{get_db_schema} = \@schema;
+}
+
+{
+    my @clear = split /;/, <<END;
+SET FOREIGN_KEY_CHECKS=0;
+DROP TABLE IF EXISTS storage_object;
+DROP TABLE IF EXISTS storage_object_attr;
+DROP TABLE IF EXISTS storage_object_xattr;
+DROP TABLE IF EXISTS instance;
+DROP TABLE IF EXISTS lock_record;
+DROP TABLE IF EXISTS volume;
+DROP TABLE IF EXISTS mountedvol;
+DROP TABLE IF EXISTS log;
+DROP TABLE IF EXISTS directory;
+DROP TABLE IF EXISTS deleted;
+DROP TABLE IF EXISTS cabinet;
+DROP TABLE IF EXISTS aliasvol;
+DROP TABLE IF EXISTS lost_instances;
+SET FOREIGN_KEY_CHECKS=1
+END
+#DROP PROCEDURE IF EXISTS getmountedvol;
+    $sql{get_db_clear} = \@clear;
+}
+
+__PACKAGE__->mk_ro_accessors( keys %sql );
+
+sub new {
+    my $class = shift;
+
+    my $self = \%sql;
+
+    bless $self, $class || ref $class;
+
+    return $self;
+}
+
+1;
+
+# ###
+# 
+# CREATE PROCEDURE getmountedvol() DETERMINISTIC
+# BEGIN
+#     DECLARE done BOOLEAN DEFAULT FALSE;
+#     DECLARE vol_idvar INT;
+#     DECLARE namevar VARCHAR(255);
+#     DECLARE hostvar VARCHAR(255);
+#     DECLARE pathvar VARCHAR(255);
+#     DECLARE allocatevar BOOLEAN;
+#     DECLARE availablevar BOOLEAN;
+#     DECLARE xattrvar BOOLEAN;
+#     DECLARE trans_level VARCHAR(255);
+#     DECLARE key_checks BOOLEAN;
+#     DECLARE cur1 CURSOR FOR SELECT vol_id, name, host, path, allocate, available, xattr FROM myvolume;
+#     DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = TRUE;
+# 
+#     -- store the okey checking state
+# --    SELECT @@FOREIGN_KEY_CHECKS INTO key_checks; 
+#     -- disable foregin check checks to prevent deadlocks on the mountedvol table
+# --    SET FOREIGN_KEY_CHECKS=0;
+# 
+#     -- make sure the temp table does not already exist... this can happy if the
+#     -- stored proc fails for some reason
+#     DROP TABLE IF EXISTS myvolume;
+#     CREATE TEMPORARY TABLE myvolume LIKE volume;
+#     INSERT INTO myvolume SELECT * FROM volume;
+# 
+#     -- store the current transaction level
+# --    SELECT @@session.tx_isolation INTO trans_level; 
+#     -- set trans level to repeatable-read so the volume table does not change
+#     -- out from under our cursor
+# --    SET @@session.tx_isolation = 'REPEATABLE-READ';
+# 
+#     -- iterate over the volume table finding the coresponding entry in the
+#     -- mount table and inserting union of the volume & mount row into the
+#     -- mountedvol table
+#     OPEN cur1;
+# 
+#     myloop: LOOP
+#         FETCH cur1 INTO vol_idvar, namevar, hostvar, pathvar, allocatevar, availablevar, xattrvar;
+#         IF `done` THEN LEAVE myloop; END IF;
+#         REPLACE INTO mountedvol
+#             SELECT mountpoint, total, used, vol_idvar, namevar, hostvar, pathvar, allocatevar, availablevar, xattrvar
+#             FROM
+#                 (SELECT *, INSTR(pathvar, mountpoint) = 1 as substring
+#                 FROM mount
+#                 HAVING substring = 1
+#                 ORDER BY substring DESC, LENGTH(mountpoint) DESC
+#                 LIMIT 1) as bar;
+#     END LOOP myloop;
+#     
+#     CLOSE cur1;
+# 
+#     -- restore the original transaction level
+# --    SET @@session.tx_isolation = trans_level;
+# 
+#     -- restore the original key checking state
+# --    SET @@FOREIGN_KEY_CHECKS = key_checks; 
+# 
+#     DROP TABLE IF EXISTS myvolume;
+# 
+# --    SET FOREIGN_KEY_CHECKS=1;
+# 
+#     COMMIT;
+# END 
+
+__DATA__
+CREATE TABLE directory (
+    dir_id BIGINT NOT NULL AUTO_INCREMENT,
+    dirname CHAR(255) NOT NULL,
+    parent_id BIGINT NOT NULL,
+    FOREIGN KEY(parent_id) REFERENCES directory(dir_id),
+    PRIMARY KEY(dir_id),
+    KEY(parent_id),
+    KEY(dirname),
+    UNIQUE(dirname, parent_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+INSERT INTO directory (dir_id, dirname, parent_id) VALUES (1, '/', 1);
+
+###
+
+CREATE TABLE storage_object (
+    so_id BIGINT NOT NULL AUTO_INCREMENT,
+    ext_id VARCHAR(255) NOT NULL,
+    ext_id_basename VARCHAR(255) NOT NULL,
+    dir_id BIGINT NOT NULL,
+    FOREIGN KEY(dir_id) REFERENCES directory(dir_id),
+    type enum('REG_FILE'),
+    PRIMARY KEY(so_id),
+    UNIQUE KEY(ext_id),
+    KEY(dir_id),
+    KEY(type)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+CREATE TABLE storage_object_attr (
+    so_id BIGINT NOT NULL AUTO_INCREMENT,
+    FOREIGN KEY(so_id) REFERENCES storage_object(so_id) ON DELETE CASCADE,
+    read_lock TINYINT DEFAULT 0 NOT NULL,
+    write_lock ENUM( 'write' ),
+    epoch TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    mtime TIMESTAMP,
+    PRIMARY KEY(so_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+CREATE TABLE storage_object_xattr (
+    so_id BIGINT NOT NULL AUTO_INCREMENT,
+    FOREIGN KEY(so_id) REFERENCES storage_object(so_id) ON DELETE CASCADE,
+    name VARCHAR(255),
+    value BLOB,
+    PRIMARY KEY(so_id, name),
+    KEY(so_id),
+    KEY(name(64))
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+CREATE TABLE lock_record (
+    so_id BIGINT NOT NULL,
+    FOREIGN KEY(so_id) REFERENCES storage_object(so_id),
+    type ENUM( 'read', 'write' ) NOT NULL,
+    epoch TIMESTAMP,
+    KEY(so_id)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+CREATE TABLE cabinet (
+    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;
+
+###
+
+CREATE TABLE volume (
+    vol_id INT NOT NULL AUTO_INCREMENT,
+    name VARCHAR(255) NOT NULL,
+    host VARCHAR(255) NOT NULL,
+    path VARCHAR(255) NOT NULL,
+    allocate BOOLEAN DEFAULT FALSE,
+    available BOOLEAN DEFAULT FALSE,
+    xattr BOOLEAN DEFAULT FALSE,
+    mountpoint VARCHAR(255) NOT NULL,
+    cab_id INT,
+    note VARCHAR(255),
+    last_modified TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, # SC: Added by Haydn with ALTER TABLE(?) on 2012-12-03
+    PRIMARY KEY(vol_id),
+    UNIQUE KEY(name),
+    UNiQUE KEY(path),
+    KEY(host(16)),
+    KEY(allocate),
+    KEY(available),
+    FOREIGN KEY(cab_id) REFERENCES cabinet(cab_id),
+    KEY(mountpoint(255))
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+CREATE TABLE mountedvol(
+    vol_id INT NOT NULL,
+    FOREIGN KEY(vol_id) REFERENCES volume(vol_id) ON DELETE CASCADE,
+    name VARCHAR(255) NOT NULL,
+    host VARCHAR(255) NOT NULL,
+    path VARCHAR(255) NOT NULL,
+    FOREIGN KEY(path) REFERENCES volume(path) ON DELETE CASCADE,
+    allocate BOOLEAN DEFAULT FALSE,
+    available BOOLEAN DEFAULT FALSE,
+    xattr BOOLEAN DEFAULT FALSE,
+    mountpoint VARCHAR(255) NOT NULL,
+    FOREIGN KEY(mountpoint) REFERENCES volume(mountpoint) ON DELETE CASCADE,
+    total BIGINT NOT NULL,
+    used BIGINT NOT NULL,
+    note VARCHAR(255),
+    PRIMARY KEY(vol_id),
+    KEY(name),
+    KEY(host),
+    KEY(path),
+    KEY(allocate),
+    KEY(available),
+    KEY(xattr),
+    KEY(mountpoint(255))
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+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,
+    so_id BIGINT NOT NULL,
+    FOREIGN KEY(so_id) REFERENCES storage_object(so_id) ON DELETE CASCADE,
+    vol_id INT NOT NULL,
+    FOREIGN KEY(vol_id) REFERENCES volume(vol_id),
+    uri VARCHAR(255) NOT NULL,
+    epoch TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    mtime TIMESTAMP,
+    PRIMARY KEY(ins_id),
+    KEY(so_id),
+    KEY(vol_id),
+    KEY(uri(40))
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+CREATE TABLE lost_instances (
+    id BIGINT NOT NULL AUTO_INCREMENT,
+    ins_id BIGINT NOT NULL DEFAULT -1,
+    so_id BIGINT NOT NULL DEFAULT -1,
+    vol_id INT NOT NULL DEFAULT -1,
+    uri CHAR(255) NOT NULL DEFAULT 'undefined',
+    epoch TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    mtime TIMESTAMP,
+    PRIMARY KEY(id),
+    KEY(so_id),
+    KEY(vol_id),
+    KEY(uri(40))
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+CREATE TABLE log (
+    timestamp TIMESTAMP,
+    hostname VARCHAR(255),
+    level VARCHAR(255),
+    sub VARCHAR(255),
+    message VARCHAR(2048) NOT NULL,
+    PRIMARY KEY(timestamp)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
+
+###
+
+CREATE TABLE deleted (
+    vol_id INT NOT NULL,
+    FOREIGN KEY(vol_id) REFERENCES volume(vol_id),
+    uri VARCHAR(255) NOT NULL,
+    timestamp TIMESTAMP,
+    PRIMARY KEY(vol_id, uri),
+    KEY(vol_id),
+    KEY(uri)
+) ENGINE=innodb DEFAULT CHARSET=latin1;
Index: /branches/eam_branches/neb-host-20250516/lib/Test/Nebulous.pm
===================================================================
--- /branches/eam_branches/neb-host-20250516/lib/Test/Nebulous.pm	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/lib/Test/Nebulous.pm	(revision 42864)
@@ -0,0 +1,155 @@
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id: Nebulous.pm,v 1.4 2008-09-11 22:35:52 jhoblitt Exp $
+
+package Test::Nebulous;
+
+use strict;
+
+our $VERSION = '0.01';
+
+use base qw( Exporter );
+
+use Cache::Memcached;
+use DBI;
+use File::Path qw( mkpath rmtree );
+use File::Temp qw( tempdir );
+use Nebulous::Server::SQL;
+
+our @EXPORT = qw( $NEB_DB $NEB_USER $NEB_PASS $NEB_MEMCACHED_SERVERS);
+
+# require use of the test database server:
+
+our $NEB_DB     = "DBI:mysql:database=test:host=localhost:mysql_socket=/var/run/mysqld/mysqld.sock";
+our $NEB_USER   = "test";
+our $NEB_PASS   = '';
+our $NEB_MEMCACHED_SERVERS = ['127.0.0.1:11211'];
+
+my $dbh = DBI->connect( $NEB_DB, $NEB_USER, $NEB_PASS );
+my $sql = Nebulous::Server::SQL->new;
+
+# suppress uninitalized warnings
+my $dir1 = "";
+my $dir2 = "";
+my $dir3 = "";
+my $dir4 = "";
+my $dir5 = "";
+my $dir6 = "";
+my $dir7 = "";
+my $dir8 = "";
+
+sub show_setup {
+    my $self = shift;
+
+    print "DB: $NEB_DB, USER: $NEB_USER, PASS: $NEB_PASS\n";
+}
+
+
+sub setup {
+    my $self = shift;
+
+    $self->cleanup;
+
+    # create directories after cleanup
+    $dir1 = tempdir( CLEANUP => 0 );
+    $dir2 = tempdir( CLEANUP => 0 );
+    $dir3 = tempdir( CLEANUP => 0 );
+    $dir4 = tempdir( CLEANUP => 0 );
+    $dir5 = tempdir( CLEANUP => 0 );
+    $dir6 = tempdir( CLEANUP => 0 );
+    $dir7 = tempdir( CLEANUP => 0 );
+    $dir8 = tempdir( CLEANUP => 0 );
+
+    foreach my $statement (@{ $sql->get_db_schema }) {
+        $dbh->do( $statement );
+    }
+
+    # generate 2 cabinets so we can replicate to a valid location
+    $dbh->do(qq{ INSERT INTO cabinet (name, site_id, location) values ("cab01", 1, "main") });
+    $dbh->do(qq{ INSERT INTO cabinet (name, site_id, location) values ("cab02", 1, "main") });
+    $dbh->do(qq{ INSERT INTO cabinet (name, site_id, location) values ("cab03", 2, "back") });
+    $dbh->do(qq{ INSERT INTO cabinet (name, site_id, location) values ("cab04", 2, "back") });
+
+    # node01 : allocate = TRUE (cab01, site 1)
+    $dbh->do(qq{ INSERT INTO volume (vol_id, cab_id, name, host, path, mountpoint, allocate, available) VALUES (1, 1, 'node01', 'node01', ?, '/', TRUE, TRUE) }, undef, $dir1);
+    $dbh->do(qq{ INSERT INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, 10e10, 10e7, note FROM volume WHERE vol_id = ? }, undef, 1);
+
+    # node02 : allocate = TRUE (cab02, site 1)
+    $dbh->do(qq{ INSERT INTO volume (vol_id, cab_id, name, host, path, mountpoint, allocate, available) VALUES (2, 2, 'node02', 'node02', ?, '/', TRUE, TRUE) }, undef, $dir2);
+    $dbh->do(qq{ INSERT INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, 10e10, 10e8, note FROM volume WHERE vol_id = ? }, undef, 2);
+
+    # node03: allocate = TRUE (cab03, site 2)
+    $dbh->do(qq{ INSERT INTO volume (vol_id, cab_id, name, host, path, mountpoint, allocate, available) VALUES (3, 3, 'node03', 'node03', ?, '/', TRUE, TRUE) }, undef, $dir3);
+    $dbh->do(qq{ INSERT INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, 10e10, 10e8, note FROM volume WHERE vol_id = ? }, undef, 3);
+
+    # node04: allocate = FALSE, available = FALSE
+    $dbh->do(qq{ INSERT INTO volume (vol_id, cab_id, name, host, path, mountpoint, allocate, available) VALUES (4, 1, 'node04', 'node04', ?, '/', FALSE, FALSE) }, undef, $dir4);
+    $dbh->do(qq{ INSERT INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, 10e10, 10e8, note FROM volume WHERE vol_id = ? }, undef, 4);
+
+    # node05: allocate = FALSE, available = TRUE
+    $dbh->do(qq{ INSERT INTO volume (vol_id, cab_id, name, host, path, mountpoint, allocate, available) VALUES (5, 1, 'node05', 'node05', ?, '/', FALSE, TRUE) }, undef, $dir5);
+    $dbh->do(qq{ INSERT INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, 10e10, 10e8, note FROM volume WHERE vol_id = ? }, undef, 5);
+
+    # node06: allocate = TRUE, available = FALSE
+    $dbh->do(qq{ INSERT INTO volume (vol_id, cab_id, name, host, path, mountpoint, allocate, available) VALUES (6, 1, 'node06', 'node06', ?, '/', TRUE, FALSE) }, undef, $dir6);
+    $dbh->do(qq{ INSERT INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, 10e10, 10e8, note FROM volume WHERE vol_id = ? }, undef, 6);
+
+    # node07: full
+    $dbh->do(qq{ INSERT INTO volume (vol_id, cab_id, name, host, path, mountpoint, allocate, available) VALUES (7, 1, 'node07', 'node07', ?, '/', TRUE, TRUE) }, undef, $dir7);
+    $dbh->do(qq{ INSERT INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, 10e10, 10e10, note FROM volume WHERE vol_id = ? }, undef, 7);
+    
+    # node08: allocate = TRUE, (cab04, site 2)
+    $dbh->do(qq{ INSERT INTO volume (vol_id, cab_id, name, host, path, mountpoint, allocate, available) VALUES (8, 4, 'node08', 'node08', ?, '/', TRUE, TRUE) }, undef, $dir8);
+    $dbh->do(qq{ INSERT INTO mountedvol SELECT vol_id,name,host,path,allocate,available,xattr,mountpoint, 10e10, 10e8, note FROM volume WHERE vol_id = ? }, undef, 8);
+
+#   $dbh->do(qq{ call getmountedvol() });
+}
+
+sub cleanup {
+    # memcached needs to be emptied between test runs
+    Cache::Memcached->new(servers => $NEB_MEMCACHED_SERVERS)->flush_all;
+    # if flush_all() turns out to be useless as feared
+#   my $memd = Cache::Memcached->new(servers => $NEB_MEMCACHED_SERVERS);
+#   my $query = $dbh->prepare("SELECT ext_id FROM storage_object");
+#   if ($query->execute) {
+#        while (my $row = $query->fetchrow_hashref) {
+#            my $ext_id = $row->{ext_id};
+#            warn "found key $ext_id\n" if $memd->get($ext_id);
+#            warn "deleted $ext_id\n" if $memd->delete($ext_id);
+#        }
+#   }
+#   $query->finish;
+
+    
+    foreach my $statement (@{ $sql->get_db_clear }) {
+        $dbh->do( $statement );
+    }
+
+    # on the first call to setup the $dir[12] will be ""
+    rmtree([$dir1], 0, 1) if -e $dir1;
+    rmtree([$dir2], 0, 1) if -e $dir2;
+    rmtree([$dir3], 0, 1) if -e $dir3;
+    rmtree([$dir4], 0, 1) if -e $dir4;
+    rmtree([$dir5], 0, 1) if -e $dir5;
+    rmtree([$dir6], 0, 1) if -e $dir6;
+    rmtree([$dir7], 0, 1) if -e $dir7;
+    rmtree([$dir8], 0, 1) if -e $dir8;
+}
+
+# function to change state of one node for tests:
+sub switch_node_state {
+    my $self = shift;
+
+    $dbh->do(qq{ UPDATE volume set available = FALSE where name = 'node02'});
+    $dbh->do(qq{ UPDATE mountedvol set available = FALSE where name = 'node02'});
+}
+
+sub get_node_dirs {
+    my $self = shift;
+
+    return ($dir1, $dir2, $dir3, $dir4, $dir5, $dir6, $dir7, $dir8);
+}
+
+1;
+
+__END__
Index: /branches/eam_branches/neb-host-20250516/scripts/bench_test.pl
===================================================================
--- /branches/eam_branches/neb-host-20250516/scripts/bench_test.pl	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/scripts/bench_test.pl	(revision 42864)
@@ -0,0 +1,41 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings FATAL => qw( all );
+
+use lib "./lib";
+
+use Benchmark qw( timethese );
+use Nebulous::Client;
+
+my $neb = Nebulous::Client->new(
+    proxy   => 'http://localhost:80/nebulous'
+);
+
+my $key = shift || 'foobar';
+
+eval { $neb->delete( $key ); };
+
+my $fh = $neb->create( $key );
+close $fh;
+
+
+timethese( -3,
+    {
+        'stat' => sub {
+            $neb->stat( $key );
+        },
+    }
+);
+
+$neb->delete( $key );
+
+timethese( -3,
+    {
+        'create/delete' => sub {
+            my $fh = $neb->create( $key );
+            close $fh;
+            $neb->delete( $key );
+        },
+    }
+);
Index: /branches/eam_branches/neb-host-20250516/scripts/dirize.pl
===================================================================
--- /branches/eam_branches/neb-host-20250516/scripts/dirize.pl	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/scripts/dirize.pl	(revision 42864)
@@ -0,0 +1,75 @@
+#!/usr/bin/env perl
+
+# query to check for duplicate directory entries
+# select count(*) from (select so.so_id, so.ext_id, so.dir_id from storage_object as so join (select dir_id, dirname, parent_id, count(*) from directory group by dirname, parent_id  having count(*) > 1) as foo on so.dir_id = foo.dir_id) as foo;
+
+use strict;
+use warnings;
+
+use Nebulous::Key qw( parse_neb_key );
+use Nebulous::Server;
+use Nebulous::Server::Config;
+use File::Basename qw( basename );
+
+my $config = Nebulous::Server::Config->new(
+    trace       => 'warn',
+);
+$config->add_db(
+    dbindex     => 0,
+    dsn         => 'DBI:mysql:database=nebulous:host=localhost',
+    dbuser      => 'nebulous',
+    dbpasswd    => '@neb@',
+);
+
+my $neb = Nebulous::Server->new_from_config($config);
+
+my $db = $neb->_db_for_index(0);
+
+my $n = 0;
+#{
+#    my $query = $db->prepare("SELECT COUNT(*) as n FROM storage_object");
+#    $query->execute;
+#    $n = $query->fetchrow_hashref->{'n'};
+#}
+
+# repair directory duplication
+#my $query = $db->prepare_cached("select so.so_id, so.ext_id, so.dir_id from storage_object as so join (select dir_id, dirname, parent_id, count(*) from directory group by dirname, parent_id  having count(*) > 1) as foo on so.dir_id = foo.dir_id limit 1000");
+
+# initial directory fill in
+my $work_query = $db->prepare_cached("SELECT so_id, ext_id, dir_id FROM storage_object AS so WHERE so.dir_id = 0 LIMIT 1000");
+
+my $update_query = $db->prepare_cached("UPDATE storage_object SET ext_id_basename = ?, dir_id = ? WHERE so_id = ?");
+
+# turn of fkeys, otherwise we can change storage_object.dir_id to a
+# non-existant value
+$db->do("SET FOREIGN_KEY_CHECKS=0");
+#$db->do("UPDATE storage_object SET dir_id = 0");
+
+# completely reset the directory table
+#$db->do("DELETE FROM directory");
+#$db->do("ALTER TABLE directory AUTO_INCREMENT = 1");
+# make sure these duplicates can't happen again
+#$db->do("alter table directory add unique key(dirname,parent_id)");
+# seed the root ('/') directory
+#$db->do("INSERT INTO directory VALUES(1, '/', 1)");
+
+my $i = 0;
+while ($work_query->execute and $work_query->rows) {
+    while (my $row = $work_query->fetchrow_hashref) {
+        $i++;
+        my $key = parse_neb_key($row->{'ext_id'});
+        my $parent_id = $neb->_resolve_dir_parent_id(key => $key, create => 1);
+
+#printf("dirizing %20s basename: %20s parent_id %10d\n", $key, basename($row->{'ext_id'}), $parent_id);
+#        printf("$i dirizing %s\n", $key->path);
+
+        $update_query->execute(basename($row->{'ext_id'}), $parent_id, $row->{'so_id'});
+    }
+    $db->commit;
+    printf("### COMMIT ###\n");
+    $work_query->finish;
+    printf("dirized $i\n");
+}
+
+$db->do("SET FOREIGN_KEY_CHECKS=1");
+$db->commit;
Index: /branches/eam_branches/neb-host-20250516/scripts/mkdirs.mana
===================================================================
--- /branches/eam_branches/neb-host-20250516/scripts/mkdirs.mana	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/scripts/mkdirs.mana	(revision 42864)
@@ -0,0 +1,26 @@
+#!/usr/bin/env mana
+
+macro mk.neb.dirs
+  if ($0 != 2)
+    echo "USAGE: mk.neb.dirs (path)"
+    exit 1
+  end
+
+  for i 0 256
+    for j 0 256
+      sprintf dir %02x/%02x $i $j
+      mkdir $1/$dir
+    end
+    echo $i
+  end
+end
+
+if ($SCRIPT)
+  if ($argv:n != 1)
+    echo "USAGE: mkdirs.mana (path)"
+    exit 1
+  end
+
+  mk.neb.dirs $argv:0
+  exit 0
+end
Index: /branches/eam_branches/neb-host-20250516/scripts/nebulous.cgi
===================================================================
--- /branches/eam_branches/neb-host-20250516/scripts/nebulous.cgi	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/scripts/nebulous.cgi	(revision 42864)
@@ -0,0 +1,15 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2004  Joshua Hoblitt
+#
+# $Id
+ 
+use strict;
+use warnings FATAL => qw( all );
+
+use SOAP::Transport::HTTP;
+use Nebulous::Server;
+   
+SOAP::Transport::HTTP::CGI
+    -> dispatch_to( 'Nebulous::Server' )
+    -> handle;
Index: /branches/eam_branches/neb-host-20250516/scripts/ptest.pl
===================================================================
--- /branches/eam_branches/neb-host-20250516/scripts/ptest.pl	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/scripts/ptest.pl	(revision 42864)
@@ -0,0 +1,49 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings FATAL => qw( all );
+
+use lib "./lib";
+
+use Nebulous::Client;
+
+my $neb = Nebulous::Client->new(
+    proxy   => 'http://localhost:80/nebulous'
+);
+
+my $key = shift || 'foobar';
+my $kids = shift || 1;
+
+foreach my $id ( 1..$kids ) {
+    my $pid = fork;
+
+    unless ( $pid )  {
+        my $fname = "${key}_$id";
+        my $fh = $neb->open_create( $fname );
+        die "can't create file $fname" unless $fh;
+
+        print $fh "fooby\n";
+
+        close $fh;
+
+        $fh = $neb->open( $fname, 'read' ) or die "can't open file";
+        close $fh;
+
+        $neb->lock( $fname, 'read' );
+        $neb->unlock( $fname, 'read' );
+        $neb->replicate( $fname );
+        $neb->cull( $fname );
+        $neb->find( $fname );
+        $neb->copy( $fname, $fname . "_copy" );
+        $neb->move( $fname, $fname . "_move" );
+        $neb->delete( $fname . "_copy" );
+        $neb->delete( $fname . "_move" );
+
+        exit 0;
+    }
+}
+
+while ( $kids ) {
+    wait();
+    $kids --;
+}
Index: /branches/eam_branches/neb-host-20250516/scripts/stest.sh
===================================================================
--- /branches/eam_branches/neb-host-20250516/scripts/stest.sh	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/scripts/stest.sh	(revision 42864)
@@ -0,0 +1,7 @@
+#!/bin/csh -f
+
+# script to run tests of the cmd-line interfaces
+
+# neb-touch
+# neb-mv
+# neb-mv
Index: /branches/eam_branches/neb-host-20250516/t/00_distribution.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/00_distribution.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/00_distribution.t	(revision 42864)
@@ -0,0 +1,25 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2005  Joshua Hoblitt
+#
+# $Id: 00_distribution.t,v 1.2 2007-05-02 01:07:56 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use lib qw( ./lib ./t );
+
+use Test::More;
+
+# example taken from Test::Distribution Pod
+
+BEGIN {
+    eval {
+        require Test::Distribution;
+    };
+    if($@) {
+        plan skip_all => 'Test::Distribution not installed';
+    } else {
+        import Test::Distribution not => [qw( versions use podcover )];
+    }
+}
Index: /branches/eam_branches/neb-host-20250516/t/01_load.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/01_load.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/01_load.t	(revision 42864)
@@ -0,0 +1,18 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 01_load.t,v 1.9 2008-03-21 01:24:00 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use lib qw( ./t ./lib );
+
+use Test::More tests => 5;
+
+BEGIN { use_ok( 'Nebulous::Key' ); }
+BEGIN { use_ok( 'Nebulous::Server' ); }
+BEGIN { use_ok( 'Nebulous::Server::Log' ); }
+BEGIN { use_ok( 'Nebulous::Server::SOAP' ); }
+BEGIN { use_ok( 'Nebulous::Server::SQL' ); }
Index: /branches/eam_branches/neb-host-20250516/t/02_config.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/02_config.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/02_config.t	(revision 42864)
@@ -0,0 +1,119 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 02_config.t,v 1.1.2.2 2008-12-14 22:52:37 eugene Exp $
+
+use strict;
+use warnings;
+
+use Test::More tests => 22;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server::Config;
+
+isa_ok(Nebulous::Server::Config->new(), "Nebulous::Server::Config");
+
+{
+    my $config = Nebulous::Server::Config->new;
+
+    ok($config->add_db(
+        dbindex     => 0,
+        dsn         => "DBI:mysql:database=foobar:host=localhost",
+        dbuser      => "baz",
+        dbpasswd    => "boo",
+    ), "add a db");
+
+    is($config->n_db, 1, "number of dbs");
+}
+
+
+{
+    my $config = Nebulous::Server::Config->new;
+
+    ok($config->add_db(
+        dbindex     => 0,
+        dsn         => "DBI:mysql:database=foobar:host=localhost",
+        dbuser      => "baz",
+        dbpasswd    => "boo",
+    ), "add a db");
+
+    ok($config->add_db(
+        dbindex     => 0,
+        dsn         => "DBI:mysql:database=foobar:host=localhost",
+        dbuser      => "baz",
+        dbpasswd    => "boo",
+    ), "add a db");
+
+    is($config->n_db, 1, "number of dbs");
+}
+
+{
+    my $config = Nebulous::Server::Config->new;
+
+    ok($config->add_db(
+        dbindex     => 0,
+        dsn         => "DBI:mysql:database=foobar:host=localhost",
+        dbuser      => "baz",
+        dbpasswd    => "boo",
+    ), "add a db");
+
+    ok($config->add_db(
+        dbindex     => 1,
+        dsn         => "DBI:mysql:database=foobar:host=localhost",
+        dbuser      => "baz",
+        dbpasswd    => "boo",
+    ), "add a db");
+
+    is($config->n_db, 2, "number of dbs");
+}
+
+{
+    my $config = Nebulous::Server::Config->new;
+
+    $config->add_db(
+        dbindex     => 0,
+        dsn         => "DBI:mysql:database=foobar:host=localhost",
+        dbuser      => "baz",
+        dbpasswd    => "boo",
+    );
+
+    $config->add_db(
+        dbindex     => 1,
+        dsn         => "DBI:mysql:database=foobar:host=localhost2",
+        dbuser      => "baz2",
+        dbpasswd    => "boo2",
+    );
+
+    # default should be 0
+    my $config_db = $config->db();
+
+    isa_ok($config_db, "Nebulous::Server::Config::DB");
+    is($config_db->dsn, "DBI:mysql:database=foobar:host=localhost", "dsn");
+    is($config_db->dbuser, "baz", "dbuser");
+    is($config_db->dbpasswd, "boo", "dbpasswd");
+
+    my $config_db0 = $config->db(0);
+
+    isa_ok($config_db0, "Nebulous::Server::Config::DB");
+    is($config_db0->dsn, "DBI:mysql:database=foobar:host=localhost", "dsn");
+    is($config_db0->dbuser, "baz", "dbuser");
+    is($config_db0->dbpasswd, "boo", "dbpasswd");
+
+    my $config_db1 = $config->db(1);
+
+    isa_ok($config_db1, "Nebulous::Server::Config::DB");
+    is($config_db1->dsn, "DBI:mysql:database=foobar:host=localhost2", "dsn");
+    is($config_db1->dbuser, "baz2", "dbuser");
+    is($config_db1->dbpasswd, "boo2", "dbpasswd");
+}
+
+# memcached_servers
+{
+    my $config = Nebulous::Server::Config->new(
+            memcached_servers => ['127.0.0.1:11211'],
+        );
+
+    is_deeply($config->memcached_servers, ['127.0.0.1:11211'], "memcached_servers");
+}
Index: /branches/eam_branches/neb-host-20250516/t/03_server_setup.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/03_server_setup.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/03_server_setup.t	(revision 42864)
@@ -0,0 +1,74 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 02_server_setup.t,v 1.8 2008-12-14 22:54:25 eugene Exp $
+
+use strict;
+use warnings;
+
+use Test::More tests => 6;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+Test::Nebulous->show_setup;
+die "test";
+
+Test::Nebulous->setup;
+
+# ->new()
+
+{
+    my $neb = Nebulous::Server->new(
+        dsn         => $NEB_DB,
+        dbuser      => $NEB_USER,
+        dbpasswd    => $NEB_PASS,
+    );
+
+    ok($neb, "set database");
+}
+
+Test::Nebulous->setup;
+
+{
+    my $neb = Nebulous::Server->new(
+        dsn         => $NEB_DB,
+        dbuser      => $NEB_USER,
+        dbpasswd    => $NEB_PASS,
+        trace       => 'off',
+    );
+
+    ok($neb, "set log level");
+}
+
+Test::Nebulous->setup;
+
+# add dbs after ->new()
+{
+    my $config = Nebulous::Server::Config->new;
+
+    ok($config->add_db(
+        dbindex    => 0,
+        dsn         => $NEB_DB,
+        dbuser      => $NEB_USER,
+        dbpasswd    => $NEB_PASS,
+    ), "add db");
+    
+    ok($config->add_db(
+        dbindex    => 1,
+        dsn         => $NEB_DB,
+        dbuser      => $NEB_USER,
+        dbpasswd    => $NEB_PASS,
+    ), "add dbs");
+
+    is($config->n_db, 2, "n dbs");
+
+    my $neb = Nebulous::Server->new_from_config($config);
+     
+    isa_ok($neb, 'Nebulous::Server');
+}
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/04_server_create_object.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/04_server_create_object.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/04_server_create_object.t	(revision 42864)
@@ -0,0 +1,613 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 03_server_create_object.t,v 1.30 2008-09-11 22:35:52 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 99;
+
+use lib qw( ./t ./lib );
+
+use File::Basename qw( basename );
+use File::ExtAttr qw( getfattr );
+use Nebulous::Server;
+use Test::Nebulous;
+use Test::URI;
+use URI::Split qw( uri_split );
+
+my $test_xattr = undef;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+use Test::DBUnit dsn => $NEB_DB, username => $NEB_USER, password => $NEB_PASS;
+
+Test::Nebulous->setup;
+{
+    # key
+    my $uri = $neb->create_object("foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+  SKIP: {
+      skip "requires xattr support", 1 unless $test_xattr;
+      is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+    }
+}
+
+Test::Nebulous->setup;
+{
+    # key
+    my $uri = $neb->create_object("neb:/foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+  SKIP: {
+      skip "requires xattr support", 1 unless $test_xattr;
+      is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+    }
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $uri = $neb->create_object("neb://node01/foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $uri = $neb->create_object("neb://node02/foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $uri = $neb->create_object("/foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), '/foo', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+{
+    # key
+    my $uri = $neb->create_object("neb:///foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $uri = $neb->create_object("/foo/");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), '/foo/', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $uri = $neb->create_object("neb:/foo/");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), '/foo/', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $uri = $neb->create_object("foo/");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), 'foo/', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $uri = $neb->create_object("foo/bar");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo/bar"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), 'foo/bar', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $uri = $neb->create_object("/foo/bar");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo/bar"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), '/foo/bar', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+
+{
+    # key, volume
+    my $uri = $neb->create_object("foo", "node01");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # volume name override
+    my $uri = $neb->create_object("neb://node02/foo", "node01");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # volume name override 
+    # OK because the volume arg overrides the key's  implied volume
+    my $uri = $neb->create_object("neb://99/foo", "node01");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # undef volume name
+    # OK because the undef is ignored
+    my $uri = $neb->create_object("neb://node01/foo", undef);
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # soft volume name request
+    my $uri = $neb->create_object("neb://~node01/foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # soft volume name request
+    my $uri = $neb->create_object("neb://node01/foo", "~node02");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # any volume name request
+    my $uri = $neb->create_object("neb://any/foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # any volume name request
+    my $uri = $neb->create_object("neb://~any/foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # any volume name request
+    my $uri = $neb->create_object("neb://node01/foo", "any");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+Test::Nebulous->setup;
+
+{
+    # any volume name request
+    my $uri = $neb->create_object("neb://node01/foo", "~any");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    ok($neb->find_instances("foo"), 'object key exists');
+    uri_scheme_ok($uri, 'file');
+}
+
+# test for properly row creation in the directories table
+
+Test::Nebulous->setup;
+
+{
+    my $key = "foo";
+    $neb->create_object($key);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        storage_object  => [so_id => 1, ext_id => $key, dir_id => 1],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "a/foo";
+    $neb->create_object($key);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        storage_object  => [so_id => 1, ext_id => $key, ext_id_basename => basename($key), dir_id => 2],
+    );
+
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "a/b/foo";
+    $neb->create_object($key);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        directory       => [dir_id => 3, dirname => 'b', parent_id => 2],
+        storage_object  => [so_id => 1, ext_id => $key, ext_id_basename => basename($key), dir_id => 3],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "a/b/c/foo";
+    $neb->create_object($key);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        directory       => [dir_id => 3, dirname => 'b', parent_id => 2],
+        directory       => [dir_id => 4, dirname => 'c', parent_id => 3],
+        storage_object  => [so_id => 1, ext_id => $key, ext_id_basename => basename($key), dir_id => 4],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key1 = "a/b/c/foo";
+    my $key2 = "foo";
+    $neb->create_object($key1);
+    $neb->create_object($key2);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        storage_object  => [so_id => 2, ext_id => $key2, ext_id_basename => basename($key2), dir_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        directory       => [dir_id => 3, dirname => 'b', parent_id => 2],
+        directory       => [dir_id => 4, dirname => 'c', parent_id => 3],
+        storage_object  => [so_id => 1, ext_id => $key1, ext_id_basename => basename($key1), dir_id => 4],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key1 = "a/b/c/foo";
+    my $key2 = "a/foo";
+    $neb->create_object($key1);
+    $neb->create_object($key2);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        storage_object  => [so_id => 2, ext_id => $key2, ext_id_basename => basename($key2), dir_id => 2],
+        directory       => [dir_id => 3, dirname => 'b', parent_id => 2],
+        directory       => [dir_id => 4, dirname => 'c', parent_id => 3],
+        storage_object  => [so_id => 1, ext_id => $key1, ext_id_basename => basename($key1), dir_id => 4],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key1 = "a/b/c/foo";
+    my $key2 = "d/foo";
+    $neb->create_object($key1);
+    $neb->create_object($key2);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        directory       => [dir_id => 3, dirname => 'b', parent_id => 2],
+        directory       => [dir_id => 4, dirname => 'c', parent_id => 3],
+        storage_object  => [so_id => 1, ext_id => $key1, ext_id_basename => basename($key1), dir_id => 4],
+        directory       => [dir_id => 5, dirname => 'd', parent_id => 1],
+        storage_object  => [so_id => 2, ext_id => $key2, ext_id_basename => basename($key2), dir_id => 5],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key1 = "a/b/c/foo";
+    my $key2 = "a/d/foo";
+    $neb->create_object($key1);
+    $neb->create_object($key2);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        directory       => [dir_id => 3, dirname => 'b', parent_id => 2],
+        directory       => [dir_id => 4, dirname => 'c', parent_id => 3],
+        storage_object  => [so_id => 1, ext_id => $key1, ext_id_basename => basename($key1), dir_id => 4],
+        directory       => [dir_id => 5, dirname => 'd', parent_id => 2],
+        storage_object  => [so_id => 2, ext_id => $key2, ext_id_basename => basename($key2), dir_id => 5],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key1 = "a/b/c/foo";
+    my $key2 = "d/a/foo";
+    $neb->create_object($key1);
+    $neb->create_object($key2);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        directory       => [dir_id => 3, dirname => 'b', parent_id => 2],
+        directory       => [dir_id => 4, dirname => 'c', parent_id => 3],
+        storage_object  => [so_id => 1, ext_id => $key1, ext_id_basename => basename($key1), dir_id => 4],
+        directory       => [dir_id => 5, dirname => 'd', parent_id => 1],
+        directory       => [dir_id => 6, dirname => 'a', parent_id => 5],
+        storage_object  => [so_id => 2, ext_id => $key2, ext_id_basename => basename($key2), dir_id => 6],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key1 = "a/b/c/foo";
+    my $key2 = "a/b/c/d/foo";
+    $neb->create_object($key1);
+    $neb->create_object($key2);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        directory       => [dir_id => 3, dirname => 'b', parent_id => 2],
+        directory       => [dir_id => 4, dirname => 'c', parent_id => 3],
+        storage_object  => [so_id => 1, ext_id => $key1, ext_id_basename => basename($key1), dir_id => 4],
+        directory       => [dir_id => 5, dirname => 'd', parent_id => 4],
+        storage_object  => [so_id => 2, ext_id => $key2, ext_id_basename => basename($key2), dir_id => 5],
+    );
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+    $neb->create_object("foo");
+};
+like($@, qr/Duplicate entry/, "object already exists");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("neb:/foo");
+    $neb->create_object("neb:/foo");
+};
+like($@, qr/Duplicate entry/, "object already exists");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo", '~node07');
+};
+like($@, qr/node07 is not available/, "request volume which is full");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("neb://~node07/foo");
+};
+like($@, qr/node07 is not available/, "request volume which is full");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo", '~node04');
+};
+like($@, qr/node04 is not available/, "request volume with allocate = FALSE, available = FALSE");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("neb://~node04/foo");
+};
+like($@, qr/node04 is not available/, "request volume with allocate = FALSE, available = FALSE");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo", '~node05');
+};
+like($@, qr/node05 is not available/, "request volume with allocate = FALSE , available = TRUE");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("neb://~node05/foo");
+};
+like($@, qr/node05 is not available/, "request volume with allocate = FALSE , available = TRUE");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo", '~node06');
+};
+like($@, qr/node06 is not available/, "request volume with allocate = TRUE, available = FALSE");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("neb://~node06/foo");
+};
+like($@, qr/node06 is not available/, "request volume with allocate = TRUE, available = FALSE");
+
+Test::Nebulous->setup;
+
+{
+    ok($neb->create_object("foo", 99));
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo", "~99");
+};
+like($@, qr/is not a valid volume name/, "volume name doesn't exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("neb://~99/foo");
+};
+like($@, qr/is not a valid volume name/, "volume name doesn't exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object();
+};
+like($@, qr/1 - 2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object(1, "node01", 3);
+};
+like($@, qr/1 - 2 were expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/05_server_replicate_object.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/05_server_replicate_object.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/05_server_replicate_object.t	(revision 42864)
@@ -0,0 +1,344 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 04_server_replicate_object.t,v 1.16 2008-09-11 22:35:52 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 43;
+
+use lib qw( ./t ./lib );
+
+use File::ExtAttr qw( getfattr );
+use Nebulous::Server;
+use Test::Nebulous;
+use Test::URI;
+use URI::Split qw( uri_split );
+
+my $test_xattr = undef;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+    memcached_servers => $NEB_MEMCACHED_SERVERS,
+);
+
+Test::Nebulous->setup;
+{
+    # key
+    $neb->create_object("foo");
+    my $uri = $neb->replicate_object("foo");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+{
+    # key, $volume
+    $neb->create_object("foo");
+    my $uri = $neb->replicate_object("foo", "node01");
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+{
+    # key, $volume is undef
+    $neb->create_object("foo");
+    my $uri = $neb->replicate_object("foo", undef);
+
+    my ($scheme, $auth, $path, $query, $frag) = uri_split($uri);
+    ok(-e $path, "file exists");
+    uri_scheme_ok($uri, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+    is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+}
+
+Test::Nebulous->setup;
+{
+    # key, $volume
+    $neb->create_object("foo");
+    my $uri1 = $neb->replicate_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo", "~node02");
+
+    {
+        my ($scheme, $auth, $path, $query, $frag) = uri_split($uri1);
+        ok(-e $path, "file exists");
+        uri_scheme_ok($uri1, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+        is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+    }
+
+    {
+        my ($scheme, $auth, $path, $query, $frag) = uri_split($uri2);
+        ok(-e $path, "file exists");
+        uri_scheme_ok($uri2, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+        is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+    }
+}
+
+Test::Nebulous->setup;
+
+{
+    # key, $volume
+    $neb->create_object("foo");
+    my $uri1 = $neb->replicate_object("foo", "any");
+
+    {
+        my ($scheme, $auth, $path, $query, $frag) = uri_split($uri1);
+        ok(-e $path, "file exists");
+        uri_scheme_ok($uri1, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+        is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+    }
+}
+
+Test::Nebulous->setup;
+
+{
+    # key, $volume
+    $neb->create_object("foo");
+    my $uri1 = $neb->replicate_object("foo", "~any");
+
+    {
+        my ($scheme, $auth, $path, $query, $frag) = uri_split($uri1);
+        ok(-e $path, "file exists");
+        uri_scheme_ok($uri1, 'file');
+
+SKIP: {
+    skip "requires xattr support", 1 unless $test_xattr;
+        is(getfattr($path, 'user.nebulous_key'), 'foo', 'user.nebulous_key xattr');
+}
+    }
+}
+
+# hard_volume tests:
+
+# replication with hard_volume:
+Test::Nebulous->setup;
+{
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # checking on replicate_object: hard_volume as node target
+    my $uri1 = $neb->create_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo", "~node02");
+
+    my $locations = $neb->find_instances("foo");
+    my $Nloc = @$locations;
+
+    is($Nloc, 2, "found 2 instances");
+    
+    my $loc1 = $locations->[0];
+    my $loc2 = $locations->[1];
+
+    like($loc1, qr/$nodedirs[0]/, "instance matches expected node");
+    like($loc2, qr/$nodedirs[1]/, "instance matches expected node");
+}
+
+# replication to unknown volume: should succeed
+Test::Nebulous->setup;
+{
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # checking on replicate_object, does this logic make sense?
+    my $uri1 = $neb->create_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo", "dummy");
+
+    my $locations = $neb->find_instances("foo");
+    my $Nloc = @$locations;
+
+    is($Nloc, 2, "found 2 instances");
+    
+    my $loc1 = $locations->[0];
+    my $loc2 = $locations->[1];
+
+    like($loc1, qr/$nodedirs[0]/, "instance matches expected node");
+    uri_scheme_ok($loc2, 'file');
+}
+
+# replication to unknown volume with hard_volume: should fail
+Test::Nebulous->setup;
+eval {
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # checking on replicate_object, does this logic make sense?
+    my $uri1 = $neb->create_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo", "~dummy");
+
+    my $locations = $neb->find_instances("foo");
+};
+like($@, qr/is not a valid volume name/, "replicate to unknown node with hard_volume should fail");
+
+# replication to unavailable volume: should succeed
+Test::Nebulous->setup;
+{
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # checking on replicate_object, does this logic make sense?
+    my $uri1 = $neb->create_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo", "node04");
+
+    my $locations = $neb->find_instances("foo");
+    my $Nloc = @$locations;
+
+    is($Nloc, 2, "found 2 instances");
+    
+    my $loc1 = $locations->[0];
+    my $loc2 = $locations->[1];
+
+    like($loc1, qr/$nodedirs[0]/, "instance matches expected node");
+    uri_scheme_ok($loc2, 'file');
+}
+
+# replication to unavailable volume with hard_volume: should fail
+Test::Nebulous->setup;
+eval {
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # checking on replicate_object, does this logic make sense?
+    my $uri1 = $neb->create_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo", "~node04");
+
+    my $locations = $neb->find_instances("foo");
+};
+like($@, qr/is not available/, "replicate to unknown node with hard_volume should fail");
+
+### END hard_volume tests
+
+### location-aware tests (do we respect the cab_id / vol_id exclusions?)
+
+# here are the volume definitions (Test/Nebulous.pm):
+# name   : vol_id : cab_id : site_id : xattr
+# node01 :      1 :      1 :       1 :     0
+# node02 :      2 :      2 :       1 :     0
+# node03 :      3 :      3 :       2 :     0
+# node04 :      4 :      1 :       1 :     0
+# node05 :      5 :      1 :       1 :     0
+# node06 :      6 :      1 :       1 :     0
+# node07 :      7 :      1 :       1 :     0
+# node08 :      8 :      4 :       2 :     0
+
+# replication without volume 
+Test::Nebulous->setup;
+{
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # checking on replicate_object: hard_volume as node target
+    my $uri1 = $neb->create_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo");
+
+    my $locations = $neb->find_instances("foo");
+    my $Nloc = @$locations;
+
+    is($Nloc, 2, "found 2 instances");
+    
+    my $loc1 = $locations->[0];
+    my $loc2 = $locations->[1];
+
+    # if the volume is not explicitly specified, then the first copy
+    # should go to a different volume, cabinet, and site
+    # if first is one node01, replication should go to node03 or node08
+    # since those both have different site_id values
+    like($loc1, qr/$nodedirs[0]/, "instance matches expected node");
+
+    my $isnode03 = $loc2 =~ m/$nodedirs[2]/;
+    my $isnode08 = $loc2 =~ m/$nodedirs[7]/;
+    ok($isnode03 || $isnode08, "instance matches expected node");
+}
+
+# replication without volume
+Test::Nebulous->setup;
+{
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # checking on replicate_object, does this logic make sense?
+    my $uri1 = $neb->create_object("foo", "~node03");
+    my $uri2 = $neb->replicate_object("foo");
+
+    my $locations = $neb->find_instances("foo");
+    my $Nloc = @$locations;
+
+    is($Nloc, 2, "found 2 instances");
+    
+    my $loc1 = $locations->[0];
+    my $loc2 = $locations->[1];
+
+    # if the volume is not explicitly specified, then the first copy
+    # should go to a different volume, cabinet, and site
+    # if first is one node03, replication should go to node01 or node02
+    # since those both have different site_id values
+    like($loc1, qr/$nodedirs[2]/, "instance matches expected node");
+
+    my $isnode01 = $loc2 =~ m/$nodedirs[0]/;
+    my $isnode02 = $loc2 =~ m/$nodedirs[1]/;
+    ok($isnode01 || $isnode02, "instance matches expected node");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->replicate_object('foo');
+};
+like($@, qr/is valid object key/, 'storage object does not exist');
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+    ok($neb->replicate_object('foo', 'bar'),'soft fake storage volume');
+};
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+    $neb->replicate_object('foo', '~bar');
+};
+like($@, qr/is not a valid volume name/, 'storage volume does not exist');
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->replicate_object();
+};
+like($@, qr/1 - 2 were expected/, 'no params');
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+    $neb->replicate_object('foo', 'node01', 3);
+};
+like($@, qr/1 - 2 were expected/, 'too many params');
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/06_server_lock_object.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/06_server_lock_object.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/06_server_lock_object.t	(revision 42864)
@@ -0,0 +1,126 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 05_server_lock_object.t,v 1.10 2008-03-20 21:10:14 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 13;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("foo");
+
+    ok($neb->lock_object("foo", "read"), "read lock");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("foo");
+
+    ok($neb->lock_object("foo", "read"), "read lock");
+    ok($neb->lock_object("foo", "read"), "read lock");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("foo");
+
+    ok($neb->lock_object("foo", "write"), "write lock");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->lock_object("foo", "read");
+};
+like($@, qr/is valid object key/, "storage object does not exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->lock_object("foo", "write");
+};
+like($@, qr/is valid object key/, "storage object does not exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo", "write");
+    $neb->lock_object("foo", "write");
+};
+like($@, qr/can not write lock twice/, "can not write lock twice");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo", "read");
+    $neb->lock_object("foo", "write");
+};
+like($@, qr/can not write lock after read lock/, "can not write lock after read lock");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo", "write");
+    $neb->lock_object("foo", "read");
+};
+like($@, qr/can not read lock after write lock/, "can not read lock after write lock");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->lock_object();
+};
+like($@, qr/2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo");
+};
+like($@, qr/2 were expected/, "not enough params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo", "both");
+};
+like($@, qr/is read or write/, "not read or write");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo", 'read', 3);
+};
+like($@, qr/2 were expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/07_server_unlock_object.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/07_server_unlock_object.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/07_server_unlock_object.t	(revision 42864)
@@ -0,0 +1,141 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 06_server_unlock_object.t,v 1.10 2008-03-20 21:10:14 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 14;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo", "read");
+
+    ok($neb->unlock_object("foo", "read"), "read unlock");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo", "read");
+    $neb->lock_object("foo", "read");
+
+    ok($neb->unlock_object("foo", "read"), "read unlock");
+    ok($neb->unlock_object("foo", "read"), "read unlock");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("foo");
+
+    $neb->lock_object("foo", "write");
+
+    ok($neb->unlock_object("foo", "write"), "write unlock");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->unlock_object("foo", "read");
+};
+like($@, qr/is valid object key/, "storage object does not exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->unlock_object("foo", "write");
+};
+like($@, qr/is valid object key/, "storage object does not exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->unlock_object("foo", "read");
+};
+like($@, qr/can not remove non-existant read lock/, "no lock set");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->unlock_object("foo", "write");
+};
+like($@, qr/can not remove non-existant write lock/, "no lock set");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+    $neb->lock_object("foo", "write");
+
+    $neb->unlock_object("foo", "read");
+};
+like($@, qr/can not have a read lock under a write lock/, "read unlock under write lock");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+    $neb->lock_object("foo", "read");
+
+    $neb->unlock_object("foo", "write");
+};
+like($@, qr/can not have a write lock under a read lock/, "write unlock under read lock");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->unlock_object();
+};
+like($@, qr/2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->unlock_object("foo");
+};
+like($@, qr/2 were expected/, "not enough params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->unlock_object("foo", "both");
+};
+like($@, qr/is read or write/, "not read or write");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->unlock_object("foo", 'read', 3);
+};
+like($@, qr/2 were expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/08_server_find_instances.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/08_server_find_instances.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/08_server_find_instances.t	(revision 42864)
@@ -0,0 +1,294 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 07_server_find_instances.t,v 1.16 2008-09-11 22:35:52 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 44;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::URI;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+# XXX previously, the logic was: if we request a hard volume location
+# using ~node, then we would give an error if no data is available except on that node.
+
+# the code has been modified so that the requested storage object is provided if possible, 
+# even if the apparently "hard" target did not exist.
+
+Test::Nebulous->setup;
+Test::Nebulous->cleanup; # make sure the database is reset before running any tests
+
+# for (my $i = 0; $i < $Nloc; $i++) {
+#	my $loc = $locations->[$i];
+#	print "location: $loc\n";
+#}
+
+Test::Nebulous->setup;
+{
+    # key
+    my $uri = $neb->create_object("foo");
+
+    my $locations = $neb->find_instances("foo");
+
+    uri_scheme_ok($locations->[0], 'file');
+    is($uri, $locations->[0], "URIs match");
+}
+
+Test::Nebulous->setup;
+{
+    # key
+    my $uri1 = $neb->create_object("foo");
+    my $uri2 = $neb->replicate_object("foo");
+
+    my $locations = $neb->find_instances("foo");
+
+    uri_scheme_ok($locations->[0], 'file');
+    uri_scheme_ok($locations->[1], 'file');
+    ok(eq_set([$uri1, $uri2], $locations), "URIs match");
+}
+
+Test::Nebulous->setup;
+{
+    # key, volume
+    my $uri = $neb->create_object('foo', 'node01');
+
+    my $locations = $neb->find_instances('foo', 'node01');
+
+    uri_scheme_ok($locations->[0], 'file');
+    is($uri, $locations->[0], "URIs match");
+}
+
+Test::Nebulous->setup;
+{
+    # key, volume
+    my $uri = $neb->create_object('foo', 'node01');
+
+    my $locations = $neb->find_instances('foo', undef);
+
+    uri_scheme_ok($locations->[0], 'file');
+    is($uri, $locations->[0], "URIs match");
+}
+
+Test::Nebulous->setup;
+{
+    # key, volume
+    my $uri = $neb->create_object('foo', 'node01');
+
+    my $locations = $neb->find_instances('neb://node02/foo', "~any");
+
+    uri_scheme_ok($locations->[0], 'file');
+    is($uri, $locations->[0], "URIs match");
+}
+
+Test::Nebulous->setup;
+{
+    # key
+    my $uri1 = $neb->create_object("foo");
+    my $uri2 = $neb->replicate_object("foo");
+
+    my $locations = $neb->find_instances("foo", "~any");
+
+    uri_scheme_ok($locations->[0], 'file');
+    uri_scheme_ok($locations->[1], 'file');
+    ok(eq_set([$uri1, $uri2], $locations), "URIs match");
+}
+
+# request from a hard_volume returns an instance which is not on the hard volume if available
+Test::Nebulous->setup;
+eval {
+    # key, volume
+    my $uri = $neb->create_object('foo', '~node01');
+    my $locations = $neb->find_instances('foo', '~node02');
+
+    uri_scheme_ok($locations->[0], 'file');
+    is($uri, $locations->[0], "URIs match");
+};
+is($@, "", "instance on different node (as hard_volume) returned");
+
+# request from a hard_volume returns an instance which is not on the hard volume if available
+Test::Nebulous->setup;
+eval {
+    # key, volume
+    my $uri = $neb->create_object('foo', '~node01');
+    my $locations = $neb->find_instances('foo', '~node02');
+
+    uri_scheme_ok($locations->[0], 'file');
+    is($uri, $locations->[0], "URIs match");
+};
+is($@, "", "instance on different node (as hard_volume) returned");
+
+### TESTS FOR INSTANCE ON INVALID NODE
+diag ("TESTS FOR INSTANCE ON INVALID NODE");
+
+# request from a soft_volume for an instance which is on an invalid node (find_invalid = 1) [SOFT, 1]
+Test::Nebulous->setup;
+eval {
+    # key, volume
+    my $uri = $neb->create_object('foo', '~node02');
+    Test::Nebulous->switch_node_state;
+
+    my $locations = $neb->find_instances('foo', 'node02', 1);
+    uri_scheme_ok($locations->[0], 'file');
+    is($uri, $locations->[0], "URIs match");
+};
+is($@, "", "instance on invalid node (as soft_volume) returned with find_invalid = 1");
+
+# request from a soft_volume for an instance which is on an invalid node (find_invalid = 0) [SOFT, 0]
+Test::Nebulous->setup;
+eval {
+    # key, volume
+    my $uri = $neb->create_object('foo', '~node02');
+    Test::Nebulous->switch_node_state;
+
+    my $locations = $neb->find_instances('foo', 'node02', 0);
+};
+like($@, qr/database error/, "instance on invalid node (as soft_volume) not returned (find_invalid = 0)");
+
+# request from a hard_volume for an instance which is on an invalid node (find_instance not supplied) [SOFT, X]
+Test::Nebulous->setup;
+eval {
+    # key, volume
+    my $uri = $neb->create_object('foo', '~node02');
+    Test::Nebulous->switch_node_state;
+
+    my $locations = $neb->find_instances('foo', 'node02');
+};
+like($@, qr/no instances available for key/, "instance on invalid node (as soft_volume) not returned (find_invalid not set)");
+
+# request from a hard_volume for an instance which is on an invalid node (find_invalid = 1) [HARD, 1]
+Test::Nebulous->setup;
+eval {
+    # key, volume
+    my $uri = $neb->create_object('foo', '~node02');
+    Test::Nebulous->switch_node_state;
+
+    my $locations = $neb->find_instances('foo', '~node02', 1);
+    uri_scheme_ok($locations->[0], 'file');
+    is($uri, $locations->[0], "URIs match");
+};
+is($@, "", "instance on invalid node (as hard_volume) returned with find_invalid = 1");
+
+# request from a hard_volume for an instance which is on an invalid node (find_invalid = 0) [HARD, 0]
+Test::Nebulous->setup;
+eval {
+    # key, volume
+    my $uri = $neb->create_object('foo', '~node02');
+    Test::Nebulous->switch_node_state;
+
+    my $locations = $neb->find_instances('foo', '~node02', 0);
+};
+like($@, qr/no instances on storage volume/, "instance on invalid node (as hard_volume) not returned (find_invalid = 0)");
+
+# request from a hard_volume for an instance which is on an invalid node [HARD, X]
+Test::Nebulous->setup;
+eval {
+    # key, volume
+    my $uri = $neb->create_object('foo', '~node02');
+    Test::Nebulous->switch_node_state;
+
+    my $locations = $neb->find_instances('foo', '~node02');
+};
+like($@, qr/no instances on storage volume/, "instance on invalid node (as hard_volume) not returned (find_invalid not set)");
+
+#### TESTING the instance proximity analysis
+Test::Nebulous->setup;
+{
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # place 4 instances on specific nodes (hard targets works):
+    my $uri1 = $neb->create_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo", "~node02");
+    my $uri3 = $neb->replicate_object("foo", "~node03");
+    my $uri4 = $neb->replicate_object("foo", "~node08");
+
+    my $locations = $neb->find_instances("foo", "node01");
+    my $Nloc = @$locations;
+
+    is($Nloc, 4, "found 4 instances");
+    
+    my $loc1 = $locations->[0];
+    my $loc2 = $locations->[1];
+    my $loc3 = $locations->[2];
+    my $loc4 = $locations->[3];
+
+    # choosing instance relative to node01 yields:
+    like($loc1, qr/$nodedirs[0]/, "node01 is closest to node01");    
+    like($loc2, qr/$nodedirs[1]/, "node02 is 2nd closest to node01");
+    like($loc3, qr/$nodedirs[2]/, "node03 is 3rd closest to node01");
+    like($loc4, qr/$nodedirs[7]/, "node08 is 4th closest to node01");
+}
+
+#### TESTING the instance proximity analysis
+Test::Nebulous->setup;
+{
+    my @nodedirs = Test::Nebulous->get_node_dirs();
+
+    # place 4 instances on specific nodes (hard targets works):
+    my $uri1 = $neb->create_object("foo", "~node01");
+    my $uri2 = $neb->replicate_object("foo", "~node02");
+    my $uri3 = $neb->replicate_object("foo", "~node03");
+    my $uri4 = $neb->replicate_object("foo", "~node08");
+
+    my $locations = $neb->find_instances("foo", "node03");
+    my $Nloc = @$locations;
+
+    is($Nloc, 4, "found 4 instances");
+    
+    my $loc1 = $locations->[0]; # 
+    my $loc2 = $locations->[1]; # 
+    my $loc3 = $locations->[2]; # 
+    my $loc4 = $locations->[3]; # 
+
+    # choosing instance relative to node01 yields:
+    like($loc1, qr/$nodedirs[2]/, "node03 is closest to node03");
+    like($loc2, qr/$nodedirs[7]/, "node08 is 2nd closest to node03");
+    like($loc3, qr/$nodedirs[1]/, "node02 is 3rd closest to node03");
+    like($loc4, qr/$nodedirs[0]/, "node08 is 4th closest to node03");
+}
+
+########
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->find_instances('foo');
+};
+like($@, qr/is valid object key/, "storage object does not exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+    $neb->find_instances('foo', '~bar');
+};
+like($@, qr/is not a valid volume name/, "storage volume does not exist");
+
+Test::Nebulous->setup;
+eval {
+    my $locations = $neb->find_instances();
+};
+like($@, qr/1 - 3 were expected/, "no params");
+# note the new API expects 1-3 parameters
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+    $neb->find_instances('foo', 'node01', 3, 5);
+};
+like($@, qr/1 - 3 were expected/, "too many params");
+# note the new API expects 1-3 parameters
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/09_server_delete_instance.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/09_server_delete_instance.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/09_server_delete_instance.t	(revision 42864)
@@ -0,0 +1,105 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 08_server_delete_instance.t,v 1.10.22.1 2008-12-14 22:52:37 eugene Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 11;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+use Test::DBUnit dsn => $NEB_DB, username => $NEB_USER, password => $NEB_PASS;
+
+Test::Nebulous->setup;
+
+{
+    my $key = "foo";
+    my $uri = $neb->create_object($key);
+
+    ok($neb->delete_instance($key, $uri), "delete instance");
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key);
+    my $uri2 = $neb->replicate_object($key);
+
+    ok($neb->delete_instance($key, $uri1), "delete instance");
+
+    my $locations = $neb->find_instances($key);
+
+    is($locations->[0], $uri2, "instance remains");
+
+    ok($neb->delete_instance($key, $uri2), "delete instance");
+
+    eval {
+        $neb->find_instances($key);
+    };
+    like($@, qr/is valid object key/, "storage object was deleted");
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key, 'node01');
+    my $uri2 = $neb->replicate_object($key, 'node02');
+
+    # make one of the instances unavailable
+    my $dbh = test_dbh();
+    $dbh->do("UPDATE mountedvol SET available = 0 WHERE name = 'node01'");
+
+    ok($neb->delete_instance($key, $uri2), "delete instance");
+
+    expected_dataset_ok(
+        deleted => [vol_id => 1, uri => $uri1],
+    );
+
+    eval {
+        $neb->find_instances($key);
+    };
+    like($@, qr/is valid object key/, "storage object was deleted");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key);
+
+    $neb->delete_instance($key, "file:/foo");
+};
+like($@, qr/no instance is associated with uri/, "uri does not exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->delete_instance();
+};
+like($@, qr/2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key);
+
+    $neb->delete_instance("foo", 2, 3);
+};
+like($@, qr/2 were expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/10_server_stat_object.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/10_server_stat_object.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/10_server_stat_object.t	(revision 42864)
@@ -0,0 +1,72 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 09_server_stat_object.t,v 1.15 2008-05-16 20:29:19 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 13;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+{
+    $neb->create_object("foo");
+
+    my $info = $neb->stat_object("foo");
+
+    is(scalar @$info, 8, "number of columns");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("foo", "node01");
+
+    my $info = $neb->stat_object("foo");
+
+    is(scalar @$info, 8,                       "number of columns");
+    is(@$info[0], 1,                           "so_id");
+    is(@$info[1], "foo",                       "ext_id");
+    is(@$info[2], 0,                           "read lock");
+    is(@$info[3], undef,                       "write lock");
+    like(@$info[4], qr/....-..-.. ..:..:../,   "epoch");
+    like(@$info[5], qr/....-..-.. ..:..:../,   "mtime");
+    is(@$info[6], 1,                           "available instances");
+    is(@$info[7], 1,                           "total instances");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    my $stat = $neb->stat_object("foo");
+};
+like($@, qr/is valid object key/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->stat_object();
+};
+like($@, qr/1 was expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+    $neb->stat_object("foo", 2);
+};
+like($@, qr/1 was expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/11_server_is_valid_volume_name.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/11_server_is_valid_volume_name.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/11_server_is_valid_volume_name.t	(revision 42864)
@@ -0,0 +1,45 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 10_server_is_valid_volume_name.t,v 1.7 2008-12-14 22:54:25 eugene Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 5;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+
+ok($neb->_is_valid_volume_name('foo', 'node01'), "valid volume name");
+
+Test::Nebulous->setup;
+
+ok($neb->_is_valid_volume_name('foo', 'node02'), "valid volume name");
+
+Test::Nebulous->setup;
+
+is($neb->_is_valid_volume_name('foo', 'node99'), 0, "invalid volume name");
+
+Test::Nebulous->setup;
+
+# key does not need to exist (is hashed to select db server)
+ok($neb->_is_valid_volume_name('bax', 'node01'), "valid volume name");
+
+Test::Nebulous->setup;
+
+# 'any' should always be a valid name
+ok($neb->_is_valid_volume_name('foo', 'any'), "valid volume name");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/12_server_is_valid_object_key.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/12_server_is_valid_object_key.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/12_server_is_valid_object_key.t	(revision 42864)
@@ -0,0 +1,37 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 11_server_is_valid_object_key.t,v 1.3 2008-03-20 21:10:14 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 2;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object('foo');
+
+    ok($neb->_is_valid_object_key('foo'), "valid object key");
+}
+
+Test::Nebulous->setup;
+
+{
+    ok(!$neb->_is_valid_object_key('foo'), "invalid object key");
+}
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/13_server_find_objects.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/13_server_find_objects.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/13_server_find_objects.t	(revision 42864)
@@ -0,0 +1,199 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 12_server_find_objects.t,v 1.4 2008-05-16 20:29:19 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 36;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+
+# search for a regex of '' should match nothing
+eval {
+    $neb->create_object("foo");
+
+    my $keys = $neb->find_objects();
+};
+like($@, qr/no keys found/, "no keys found");
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("foo");
+
+    my $keys = $neb->find_objects("foo");
+
+    is(scalar @$keys, 1, 'number of keys found');
+    is($keys->[0], "foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    $neb->create_object("foo");
+    $neb->replicate_object("foo");
+
+    my $keys = $neb->find_objects("foo");
+
+    is(scalar @$keys, 1, 'number of keys found');
+    is($keys->[0], "foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    # key
+    $neb->create_object("foo");
+    $neb->create_object("bar");
+
+    my $keys = $neb->find_objects("foo");
+
+    is(scalar @$keys, 1, 'number of keys found');
+    is($keys->[0], "foo", "key name");
+}
+
+# test recursive dir searching
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/foo");
+
+    my $keys = $neb->find_objects("a");
+
+    is(scalar @$keys, 1, 'number of keys found');
+    is($keys->[0], "a/foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/foo");
+    $neb->create_object("b/foo");
+
+    my $keys = $neb->find_objects("a");
+
+    is(scalar @$keys, 1, 'number of keys found');
+    is($keys->[0], "a/foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/foo");
+    $neb->create_object("a/b/foo");
+
+    my $keys = $neb->find_objects("a");
+
+    is(scalar @$keys, 2, 'number of keys found');
+    is($keys->[0], "a/b/", "key name");
+    is($keys->[1], "a/foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/foo");
+    $neb->create_object("a/bar");
+
+    my $keys = $neb->find_objects("a");
+
+    is(scalar @$keys, 2, 'number of keys found');
+    is($keys->[0], "a/bar", "key name");
+    is($keys->[1], "a/foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/foo");
+    $neb->create_object("bar");
+
+    my $keys = $neb->find_objects("a");
+
+    is(scalar @$keys, 1, 'number of keys found');
+    is($keys->[0], "a/foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/foo");
+    $neb->create_object("foo");
+    $neb->create_object("bar");
+
+    my $keys = $neb->find_objects("/");
+
+    is(scalar @$keys, 3, 'number of keys found');
+    is($keys->[0], "a/", "key name");
+    is($keys->[1], "bar", "key name");
+    is($keys->[2], "foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/foo");
+    $neb->create_object("foo");
+    $neb->create_object("bar");
+
+    my $keys = $neb->find_objects(".");
+
+    is(scalar @$keys, 3, 'number of keys found');
+    is($keys->[0], "a/", "key name");
+    is($keys->[1], "bar", "key name");
+    is($keys->[2], "foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/foo");
+    $neb->create_object("foo");
+    $neb->create_object("bar");
+
+    my $keys = $neb->find_objects("..");
+
+    is(scalar @$keys, 3, 'number of keys found');
+    is($keys->[0], "a/", "key name");
+    is($keys->[1], "bar", "key name");
+    is($keys->[2], "foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+{
+    $neb->create_object("a/bar");
+    $neb->create_object("b/foo");
+    $neb->create_object("foo");
+
+    my $keys = $neb->find_objects("/");
+
+    is(scalar @$keys, 3, 'number of keys found');
+    is($keys->[0], "a/", "key name");
+    is($keys->[1], "b/", "key name");
+    is($keys->[2], "foo", "key name");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->find_objects("foo", 3);
+};
+like($@, qr/1 was expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/14_server_rename_object.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/14_server_rename_object.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/14_server_rename_object.t	(revision 42864)
@@ -0,0 +1,122 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2007  Joshua Hoblitt
+#
+# $Id: 13_server_rename_object.t,v 1.5 2008-05-16 20:29:19 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 8;
+
+use lib qw( ./t ./lib );
+
+use File::Basename qw( basename );
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+use Test::DBUnit dsn => $NEB_DB, username => $NEB_USER, password => $NEB_PASS;
+
+Test::Nebulous->setup;
+
+{
+    my $key = "bar";
+    my $uri = $neb->create_object("foo");
+
+    $neb->rename_object("foo", $key);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        storage_object  => [so_id => 1, ext_id => $key, ext_id_basename => basename($key), dir_id => 1],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "a/bar";
+    my $uri = $neb->create_object("foo");
+
+    $neb->rename_object("foo", $key);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        storage_object  => [so_id => 1, ext_id => $key, ext_id_basename => basename($key), dir_id => 2],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "bar";
+    my $uri = $neb->create_object("a/foo");
+
+    $neb->rename_object("a/foo", $key);
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        storage_object  => [so_id => 1, ext_id => $key, ext_id_basename => basename($key), dir_id => 1],
+    );
+}
+
+Test::Nebulous->setup;
+
+# destination key exists
+eval {
+    $neb->create_object('foo');
+    $neb->create_object('bar');
+
+    $neb->rename_object('foo', 'bar');
+};
+like($@, qr/is not valid object key/, "too few params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->rename_object();
+};
+like($@, qr/2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->rename_object("foo");
+};
+like($@, qr/2 were expected/, "too few params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->rename_object("foo", "bar", "baz");
+};
+like($@, qr/2 were expected/, "too many params");
+
+# test attempting to rename a key in such a way to cause the distributed
+# storage db to change
+# this must be the last test as we're messing with the $neb object
+eval {
+    $neb->config->add_db(
+        dbindex     => 1,
+        dsn         => $NEB_DB,
+        dbuser      => $NEB_USER,
+        dbpasswd    => $NEB_PASS,
+    );
+
+    $neb->create_object("a/foo");
+    $neb->rename_object("a/foo", "g/bar");
+};
+like($@, qr/rename objects across distributed database boundaries/, "rename between databases");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/15_server_xattr.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/15_server_xattr.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/15_server_xattr.t	(revision 42864)
@@ -0,0 +1,285 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2007  Joshua Hoblitt
+#
+# $Id: 14_server_xattr.t,v 1.8 2008-07-09 23:32:35 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 44;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+# 1 key / xattr
+
+Test::Nebulous->setup;
+
+{
+    my $uri = $neb->create_object('foo');
+
+    ok($neb->setxattr_object('foo', 'user.bar', 'baz', 'create'), 'set object xattr');
+    {
+        my $xattrs = $neb->listxattr_object('foo');
+        is(scalar @$xattrs, 1, 'number of xattrs');
+        is(@$xattrs[0], 'user.bar', 'xattr name');
+    }
+
+    my $value = $neb->getxattr_object('foo', 'user.bar');
+    is($value, 'baz', 'xattr value');
+
+    ok($neb->removexattr_object('foo', 'user.bar'), "remove object xattr");
+    {
+        my $xattrs = $neb->listxattr_object('foo');
+        is(scalar @$xattrs, 0, 'number of xattrs');
+    }
+}
+
+# multiple xattrs
+
+Test::Nebulous->setup;
+
+{
+    my $uri = $neb->create_object('foo');
+
+    ok($neb->setxattr_object('foo', 'user.bar', 'baz', 'create'), 'set object xattr');
+    ok($neb->setxattr_object('foo', 'user.bonk', 'quix', 'create'), 'set object xattr');
+    
+    {
+        my $xattrs = $neb->listxattr_object('foo');
+        is(scalar @$xattrs, 2, 'number of xattrs');
+        is(@$xattrs[0], 'user.bar', 'xattr name');
+        is(@$xattrs[1], 'user.bonk', 'xattr name');
+    }
+
+    my $value = $neb->getxattr_object('foo', 'user.bar');
+    is($value, 'baz', 'xattr value');
+    $value = $neb->getxattr_object('foo', 'user.bonk');
+    is($value, 'quix', 'xattr value');
+
+    ok($neb->removexattr_object('foo', 'user.bar'), "remove object xattr");
+    ok($neb->removexattr_object('foo', 'user.bonk'), "remove object xattr");
+    {
+        my $xattrs = $neb->listxattr_object('foo');
+        is(scalar @$xattrs, 0, 'number of xattrs');
+    }
+}
+
+# replace xattrs
+
+Test::Nebulous->setup;
+
+{
+    my $uri = $neb->create_object('foo');
+
+    ok($neb->setxattr_object('foo', 'user.bar', 'baz', 'create'), 'set object xattr');
+    ok($neb->setxattr_object('foo', 'user.bar', 'quix', 'replace'), 're-set object xattr');
+    
+    {
+        my $xattrs = $neb->listxattr_object('foo');
+        is(scalar @$xattrs, 1, 'number of xattrs');
+        is(@$xattrs[0], 'user.bar', 'xattr name');
+    }
+
+    my $value = $neb->getxattr_object('foo', 'user.bar');
+    is($value, 'quix', 'xattr value');
+
+    ok($neb->removexattr_object('foo', 'user.bar'), "remove object xattr");
+    {
+        my $xattrs = $neb->listxattr_object('foo');
+        is(scalar @$xattrs, 0, 'number of xattrs');
+    }
+}
+
+# setxattr_object
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->setxattr_object('foo', 'user.bar', 'baz', 'create');
+};
+like($@, qr/is valid object key/, "create xattr on non-existant key");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->setxattr_object('foo', 'luser.bar', 'baz', 'create');
+};
+like($@, qr/xattr is in user. namespace/, "user. namspace");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->setxattr_object('foo', 'user.bar', 'baz', 'replace');
+};
+like($@, qr/is valid object key/, "replace xattr on non-existant key");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->setxattr_object();
+};
+like($@, qr/4 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->setxattr_object('foo', 'user.bar');
+};
+like($@, qr/4 were expected/, "too few params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->setxattr_object('foo', 'user.bar', 'baz');
+};
+like($@, qr/4 were expected/, "too few params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->setxattr_object('foo', 'user.bar', 'baz', 'create', 'quix');
+};
+like($@, qr/4 were expected/, "too many params");
+
+# getxattr_object
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->getxattr_object('foo', 'bar');
+};
+like($@, qr/is valid object key/, "get xattr from non-existant nebulous key");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->getxattr_object('foo', 'luser.bar');
+};
+like($@, qr/xattr is in user. namespace/, "user. namespace");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+    $neb->getxattr_object('foo', 'user.bar');
+};
+like($@, qr|xattr neb:///foo:user.bar does not exist|, "get xattr from non-existant xattr key");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->getxattr_object();
+};
+like($@, qr/2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->getxattr_object('foo');
+};
+like($@, qr/2 were expected/, "too few params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->getxattr_object('foo', 'user.bar', 'baz');
+};
+like($@, qr/2 were expected/, "too many params");
+
+# listxattr_object
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->listxattr_object();
+};
+like($@, qr/1 was expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->listxattr_object('foo', 'bar');
+};
+like($@, qr/1 was expected/, "too many params");
+
+# removexattr_object
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->removexattr_object('foo', 'user.bar');
+};
+like($@, qr/is valid object key/, "remove xattr from non-existant nebulous key");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->getxattr_object('foo', 'luser.bar');
+};
+like($@, qr/xattr is in user. namespace/, "user. namespace");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+    $neb->removexattr_object('foo', 'user.bar');
+};
+like($@, qr|xattr neb:///foo:user.bar does not exist|,
+    "remove xattr from non-existant xattr key");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->removexattr_object();
+};
+like($@, qr/2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->removexattr_object('foo');
+};
+like($@, qr/2 were expected/, "too few params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->removexattr_object('foo', 'user.bar', 'baz');
+};
+like($@, qr/2 were expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/16_mounts.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/16_mounts.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/16_mounts.t	(revision 42864)
@@ -0,0 +1,67 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2008  Joshua Hoblitt
+#
+# $Id: 15_mounts.t,v 1.3 2008-09-11 22:35:52 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 18;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::URI;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+
+{
+    # key
+    my $mounts = $neb->mounts();
+
+    is(scalar @$mounts, 8, "number of rows");
+
+    my %row;
+    # first row
+    @row{qw(mountpoint total used vol_id name host path allocate available xattr)}
+        = @{$mounts->[0]};
+    
+    is($row{total},     100000000000, "total bytes");
+    is($row{used},      100000000, "used bytes");
+    is($row{vol_id},    1, "vol id");
+    is($row{name},      "node01", "name");
+    is($row{host},      "node01", "host");
+    is($row{allocate},  1, "is allocated");
+    is($row{available}, 1, "is available");
+    is($row{xattr},     0, "has no xattr");
+
+    # 2nd row
+    @row{qw(mountpoint total used vol_id name host path allocate available xattr)}
+        = @{$mounts->[1]};
+
+    is($row{total},     100000000000, "total bytes");
+    is($row{used},      1000000000, "used bytes");
+    is($row{vol_id},    2, "vol id");
+    is($row{name},      "node02", "name");
+    is($row{host},      "node02", "host");
+    is($row{allocate},  1, "is allocated");
+    is($row{available}, 1, "is available");
+    is($row{xattr},     0, "has no xattr");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->mounts("foo");
+};
+like($@, qr/0 were expected/, "no params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/17_server_swap_objects.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/17_server_swap_objects.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/17_server_swap_objects.t	(revision 42864)
@@ -0,0 +1,117 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2007  Joshua Hoblitt
+#
+# $Id: 16_server_swap_objects.t,v 1.1 2008-10-13 20:41:17 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 13;
+
+use lib qw( ./t ./lib );
+
+use File::Basename qw( basename );
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+use Test::DBUnit dsn => $NEB_DB, username => $NEB_USER, password => $NEB_PASS;
+
+Test::Nebulous->setup;
+
+{
+    my $key1 = "foo1";
+    my $key2 = "foo2";
+    my $uri1 = $neb->create_object($key1);
+    my $uri2 = $neb->create_object($key2);
+
+    ok($neb->swap_objects($key1, $key2), "swap succeeded");
+
+    my $new_uri1 = ($neb->find_instances($key1))->[0];
+    my $new_uri2 = ($neb->find_instances($key2))->[0];
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        storage_object  => [so_id => 1, ext_id => $key2, ext_id_basename => basename($key2), dir_id => 1],
+        storage_object  => [so_id => 2, ext_id => $key1, ext_id_basename => basename($key1), dir_id => 1],
+    );
+
+    is($uri1, $new_uri2, "key1 -> key2");
+    is($uri2, $new_uri1, "key2 -> key1");
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key1 = "foo1";
+    my $key2 = "a/foo2";
+    my $uri1 = $neb->create_object($key1);
+    my $uri2 = $neb->create_object($key2);
+
+    ok($neb->swap_objects($key1, $key2), "swap succeeded");
+
+    my $new_uri1 = ($neb->find_instances($key1))->[0];
+    my $new_uri2 = ($neb->find_instances($key2))->[0];
+
+    expected_dataset_ok(
+        directory       => [dir_id => 1, dirname => '/', parent_id => 1],
+        directory       => [dir_id => 2, dirname => 'a', parent_id => 1],
+        storage_object  => [so_id => 1, ext_id => $key2, ext_id_basename => basename($key2), dir_id => 2],
+        storage_object  => [so_id => 2, ext_id => $key1, ext_id_basename => basename($key1), dir_id => 1],
+    );
+
+    is($uri1, $new_uri2, "key1 -> key2");
+    is($uri2, $new_uri1, "key2 -> key1");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('bar');
+
+    $neb->swap_objects('foo', 'bar');
+};
+like($@, qr/is valid object key/, "key1 doesn't exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object('foo');
+
+    $neb->swap_objects('foo', 'bar');
+};
+like($@, qr/is valid object key/, "key2 doesn't exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->swap_objects();
+};
+like($@, qr/2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->swap_objects("foo");
+};
+like($@, qr/2 were expected/, "too few params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+    $neb->create_object("bar");
+
+    $neb->swap_objects("foo", "bar", "baz");
+};
+like($@, qr/2 were expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/18_server_chmod_object.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/18_server_chmod_object.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/18_server_chmod_object.t	(revision 42864)
@@ -0,0 +1,121 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2009  Joshua Hoblitt
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 13;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::URI;
+use Test::Nebulous;
+use URI;
+use File::stat;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+
+# object with one instance
+{
+    my $key = "foo";
+    my $uri = $neb->create_object($key);
+
+    my $path = URI->new($uri)->path;
+    my $st1 = stat($path);
+
+    my $mode = $neb->chmod_object($key, 0440);
+    my $st2 = stat($path);
+
+    is($st2->mode &07777, 0440, "chmod() single instance");
+    is($mode, 0440, "returned mode");
+    is($neb->getxattr_object($key, 'user.mode'), 0440, "xattr user.mode");
+
+    # reset to 600 so the user can delete the file and /tmp entry
+    chmod(0600, $path);
+}
+
+Test::Nebulous->setup;
+
+# object with two instances
+{
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key);
+    my $uri2 = $neb->replicate_object($key);
+
+    my $mode = $neb->chmod_object($key, 0440);
+
+    my $path1 = URI->new($uri1)->path;
+    my $path2 = URI->new($uri2)->path;
+
+    my $st1 = stat($path1);
+    my $st2 = stat($path2);
+
+    is($st1->mode &07777, 0440, "chmod() first instance");
+    is($st2->mode &07777, 0440, "chmod() second instance");
+    is($mode, 0440, "returned mode");
+    is($neb->getxattr_object($key, 'user.mode'), 0440, "xattr user.mode");
+
+    # reset to 600 so the user can delete the file and /tmp entry
+    chmod (0600, $path1);
+    chmod (0600, $path2);
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->chmod_object("foo", 0644);
+};
+like($@, qr/valid object/, "object does not exist");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->chmod_object("foo", 0640);
+};
+like($@, qr/allowable mode/, "mode is not 0400");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->chmod_object("foo", 99999);
+};
+like($@, qr/allowable mode/, "bad mode");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->chmod_object();
+};
+like($@, qr/2 were expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->chmod_object("foo");
+};
+like($@, qr/2 were expected/, "one param");
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->create_object("foo");
+
+    $neb->chmod_object("foo", 0440, 2);
+};
+like($@, qr/2 were expected/, "three params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/19_server_prune_object.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/19_server_prune_object.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/19_server_prune_object.t	(revision 42864)
@@ -0,0 +1,73 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 08_server_delete_instance.t,v 1.10.22.1 2008-12-14 22:52:37 eugene Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 6;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+use Test::DBUnit dsn => $NEB_DB, username => $NEB_USER, password => $NEB_PASS;
+
+Test::Nebulous->setup;
+
+{
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key, 'node01');
+    my $uri2 = $neb->replicate_object($key, 'node02');
+
+    # make one of the instances unavailable
+    my $dbh = test_dbh();
+    $dbh->do("UPDATE mountedvol SET available = 0 WHERE name = 'node01'");
+
+    ok($neb->prune_object($key), "prune object");
+
+    expected_dataset_ok(
+        deleted => [vol_id => 1, uri => $uri1],
+    );
+    
+    expected_dataset_ok(
+        instance => [ins_id => 2, so_id => 1, vol_id => 2, uri => $uri2],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key);
+
+    is($neb->prune_object($key), 0, "no dead instances to remove");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->prune_object();
+};
+like($@, qr/1 was expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key);
+
+    $neb->prune_object("foo", 2);
+};
+like($@, qr/1 was expected/, "too many params");
+
+Test::Nebulous->cleanup;
Index: /branches/eam_branches/neb-host-20250516/t/20_server_find_objects_wildcard.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/20_server_find_objects_wildcard.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/20_server_find_objects_wildcard.t	(revision 42864)
@@ -0,0 +1,109 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 12_server_find_objects.t,v 1.4 2008-05-16 20:29:19 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 22;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+Test::Nebulous->setup;
+Test::Nebulous->cleanup;
+
+&mktestfiles ($neb);
+{
+    my $keys = $neb->find_objects_wildcard("topdir");
+
+    is(scalar @$keys, 4, 'number of keys found');
+    is($keys->[0], "topdir/subdir/", "key name matches");
+    is($keys->[1], "topdir/object1", "key name matches");
+    is($keys->[2], "topdir/object2", "key name matches");
+    is($keys->[3], "topdir/objfoobar", "key name matches");
+}
+
+&mktestfiles ($neb);
+{
+    my $keys = $neb->find_objects_wildcard("topdir/%");
+
+    is(scalar @$keys, 4, 'number of keys found');
+    is($keys->[0], "topdir/subdir/", "key name matches");
+    is($keys->[1], "topdir/object1", "key name matches");
+    is($keys->[2], "topdir/object2", "key name matches");
+    is($keys->[3], "topdir/objfoobar", "key name matches");
+}
+
+&mktestfiles ($neb);
+{
+    my $keys = $neb->find_objects_wildcard("topdir/obj%");
+
+    is(scalar @$keys, 3, 'number of keys found');
+    is($keys->[0], "topdir/object1", "key name matches");
+    is($keys->[1], "topdir/object2", "key name matches");
+    is($keys->[2], "topdir/objfoobar", "key name matches");
+}
+
+&mktestfiles ($neb);
+{
+    my $keys = $neb->find_objects_wildcard("topdir/object%");
+
+    is(scalar @$keys, 2, 'number of keys found');
+    is($keys->[0], "topdir/object1", "key name matches");
+    is($keys->[1], "topdir/object2", "key name matches");
+}
+
+&mktestfiles ($neb);
+{
+    my $keys = $neb->find_objects_wildcard("obj%");
+
+    is(scalar @$keys, 3, 'number of keys found');
+    is($keys->[0], "object1", "key name matches");
+    is($keys->[1], "object2", "key name matches");
+    is($keys->[2], "objfoobar", "key name matches");
+}
+
+eval {
+    $neb->find_objects_wildcard("foo", 3);
+};
+like($@, qr/1 was expected/, "too many params");
+
+Test::Nebulous->cleanup;
+
+sub mktestfiles {
+    my ($neb) = $_[0];
+
+    Test::Nebulous->setup;
+    
+    # find_objects_wildcard allows these searches:
+    # -> dir : implies dir/* (dir/%)
+    # -> dir/%
+    # -> dir/sub%
+    # -> sub%
+    
+    # create examples matching the above
+    $neb->create_object("object1");
+    $neb->create_object("object2");
+    $neb->create_object("objfoobar");
+    
+    $neb->create_object("topdir/object1");
+    $neb->create_object("topdir/object2");
+    $neb->create_object("topdir/objfoobar");
+    
+    $neb->create_object("topdir/subdir/object1");
+    $neb->create_object("topdir/subdir/object2");
+    $neb->create_object("topdir/subdir/objfoobar");
+    
+    return 1;
+}
Index: /branches/eam_branches/neb-host-20250516/t/75_parse_neb_key.t
===================================================================
--- /branches/eam_branches/neb-host-20250516/t/75_parse_neb_key.t	(revision 42864)
+++ /branches/eam_branches/neb-host-20250516/t/75_parse_neb_key.t	(revision 42864)
@@ -0,0 +1,367 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 75_parse_neb_key.t,v 1.5 2008-09-09 02:18:47 jhoblitt Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More;
+
+plan tests => 106;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Key qw( parse_neb_key );
+use Test::Nebulous;
+
+# neb://<volume name>/...
+# neb:path...
+# neb:/path... (same as neb:path, leading '/' is stripped)
+# neb:///path... (same as neb:path)
+
+# key
+{
+    my $key = parse_neb_key('foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('/foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('//foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('///foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('foo////bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('foo/bar/baz/quix/');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+# key w/ volume argument
+{
+    my $key = parse_neb_key('foo/bar/baz/quix', 'boing');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, 'boing', 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('foo/bar/baz/quix', '~boing');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, 'boing', 'volume name');
+    is($key->hard_volume, 1, 'soft volume name');
+}
+
+# URI w/ volume name
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo//bar/baz/quix');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo///bar/baz/quix');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo/bar///baz/quix');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix/');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+# URI w/ hard volume name
+{
+    my $key = parse_neb_key('neb://~foo/bar/baz/quix/');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->hard_volume, 1, 'soft volume name');
+}
+
+# URI w/ volume name and volume argument
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix', 'boing');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'boing', 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix', '~boing');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'boing', 'volume name');
+    is($key->hard_volume, 1, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix', undef);
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+# URI w/ ~any volume name
+{
+    my $key = parse_neb_key('neb://~any/bar/baz/quix/');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+# URI w/ any volume argument
+{
+    my $key = parse_neb_key('neb://boing/bar/baz/quix/', 'any');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://boing/bar/baz/quix/', '~any');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+# URI w/ hard volume name
+{
+    my $key = parse_neb_key('neb://~foo/bar/baz/quix/');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->hard_volume, 1, 'soft volume name');
+}
+
+# URI w/o volume name
+
+{
+    my $key = parse_neb_key('neb:/foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb:///foo/bar/baz/quix');
+
+    is($key->volume, undef, 'volume name');
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://///foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+# root volume references
+{
+    my $key = parse_neb_key('neb:///');
+
+    is($key->path, '', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb:///.');
+
+    is($key->path, '', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb:///..');
+
+    is($key->path, '', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('/');
+
+    is($key->path, '', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('.');
+
+    is($key->path, '', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('..');
+
+    is($key->path, '', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->hard_volume, undef, 'soft volume name');
+}
+
+# stringification
+
+{
+    my $txt = 'neb:///bar/baz';
+    my $key = parse_neb_key($txt);
+
+    is("$key", $txt, "stringified");
+}
+
+{
+    my $txt = 'neb://foo.0/bar/baz';
+    my $key = parse_neb_key($txt);
+
+    is("$key", $txt, "stringified");
+}
+
+{
+    my $txt = 'neb://~foo.0/bar/baz';
+    my $key = parse_neb_key($txt);
+
+    is("$key", $txt, "stringified");
+}
+
+# key w/ whitespace
+eval {
+    parse_neb_key('/ foo/bar/baz/quix');
+};
+like( $@, qr/may not contain whitespace/, "whitespace" );
+
+eval {
+    parse_neb_key(' /foo/bar/baz/quix');
+};
+like( $@, qr/may not contain whitespace/, "whitespace" );
+
+eval {
+    parse_neb_key('/foo/bar/baz/quix ');
+};
+like( $@, qr/may not contain whitespace/, "whitespace" );
+
+# URI w/ whitespace
+
+eval {
+    parse_neb_key('neb ://foo/bar/baz/quix');
+};
+like( $@, qr/may not contain whitespace/, "whitespace" );
+
+eval {
+    parse_neb_key('neb:// foo/bar/baz/quix');
+};
+like( $@, qr/may not contain whitespace/, "whitespace" );
+
+eval {
+    parse_neb_key(' neb://foo/bar/baz/quix');
+};
+like( $@, qr/may not contain whitespace/, "whitespace" );
+
+eval {
+    parse_neb_key('neb://foo/bar/baz/quix ');
+};
+like( $@, qr/may not contain whitespace/, "whitespace" );
+
+# URI w/o volume requires leading slash
+eval {
+    my $key = parse_neb_key('neb:foo/bar/baz/quix');
+};
+like( $@, qr/requires a leading slash/, "leading slash" );
+
+# URI w/ volume but w/o path
+eval {
+    my $key = parse_neb_key('neb://foo');
+};
+like( $@, qr/requires a path/, "no path" );
+
+# params
+eval {
+    my $key = parse_neb_key();
+};
+like( $@, qr/key param is not optional/, "no params" );
+
+eval {
+    my $key = parse_neb_key(undef);
+};
+like( $@, qr/key param is not optional/, "key is undef" );
+
+eval {
+    my $key = parse_neb_key(undef, 'bar');
+};
+like( $@, qr/key param is not optional/, "key is undef" );
+
+eval {
+    my $key = parse_neb_key('foo', 'bar', 'foo');
+};
+like( $@, qr/too many params/, "too many params" );
