summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/docs/basic_usage.rst41
-rw-r--r--lib/spack/docs/config_yaml.rst11
-rw-r--r--lib/spack/llnl/util/lock.py30
-rw-r--r--lib/spack/spack/database.py2
-rw-r--r--lib/spack/spack/main.py78
-rw-r--r--lib/spack/spack/schema/config.py1
-rw-r--r--lib/spack/spack/stage.py4
-rw-r--r--lib/spack/spack/test/llnl/util/lock.py105
-rw-r--r--lib/spack/spack/util/file_cache.py2
-rw-r--r--lib/spack/spack/util/lock.py91
10 files changed, 311 insertions, 54 deletions
diff --git a/lib/spack/docs/basic_usage.rst b/lib/spack/docs/basic_usage.rst
index bb426b4378..1a7576c74e 100644
--- a/lib/spack/docs/basic_usage.rst
+++ b/lib/spack/docs/basic_usage.rst
@@ -1093,22 +1093,43 @@ several variants:
Filesystem requirements
-----------------------
-Spack currently needs to be run from a filesystem that supports
+By default, Spack needs to be run from a filesystem that supports
``flock`` locking semantics. Nearly all local filesystems and recent
-versions of NFS support this, but parallel filesystems may be mounted
-without ``flock`` support enabled. You can determine how your
-filesystems are mounted with ``mount -p``. The output for a Lustre
+versions of NFS support this, but parallel filesystems or NFS volumes may
+be configured without ``flock`` support enabled. You can determine how
+your filesystems are mounted with ``mount``. The output for a Lustre
filesystem might look like this:
.. code-block:: console
- $ mount -l | grep lscratch
- pilsner-mds1-lnet0@o2ib100:/lsd on /p/lscratchd type lustre (rw,nosuid,noauto,_netdev,lazystatfs,flock)
- porter-mds1-lnet0@o2ib100:/lse on /p/lscratche type lustre (rw,nosuid,noauto,_netdev,lazystatfs,flock)
+ $ mount | grep lscratch
+ mds1-lnet0@o2ib100:/lsd on /p/lscratchd type lustre (rw,nosuid,lazystatfs,flock)
+ mds2-lnet0@o2ib100:/lse on /p/lscratche type lustre (rw,nosuid,lazystatfs,flock)
-Note the ``flock`` option on both Lustre mounts. If you do not see
-this or a similar option for your filesystem, you may need ot ask your
-system administrator to enable ``flock``.
+Note the ``flock`` option on both Lustre mounts.
+
+If you do not see this or a similar option for your filesystem, you have
+a few options. First, you can move your Spack installation to a
+filesystem that supports locking. Second, you could ask your system
+administrator to enable ``flock`` for your filesystem.
+
+If none of those work, you can disable locking in one of two ways:
+
+ 1. Run Spack with the ``-L`` or ``--disable-locks`` option to disable
+ locks on a call-by-call basis.
+ 2. Edit :ref:`config.yaml <config-yaml>` and set the ``locks`` option
+ to ``false`` to always disable locking.
+
+.. warning::
+
+ If you disable locking, concurrent instances of Spack will have no way
+ to avoid stepping on each other. You must ensure that there is only
+ **one** instance of Spack running at a time. Otherwise, Spack may end
+ up with a corrupted database file, or you may not be able to see all
+ installed packages in commands like ``spack find``.
+
+ If you are unfortunate enough to run into this situation, you may be
+ able to fix it by running ``spack reindex``.
This issue typically manifests with the error below:
diff --git a/lib/spack/docs/config_yaml.rst b/lib/spack/docs/config_yaml.rst
index da760f05e3..35bc22d72e 100644
--- a/lib/spack/docs/config_yaml.rst
+++ b/lib/spack/docs/config_yaml.rst
@@ -151,6 +151,17 @@ to ``false`` to disable these checks. Disabling this can expose you to
attacks. Use at your own risk.
--------------------
+``locks``
+--------------------
+
+When set to ``true``, concurrent instances of Spack will use locks to
+avoid modifying the install tree, database file, etc. If false, Spack
+will disable all locking, but you must **not** run concurrent instances
+of Spack. For filesystems that don't support locking, you should set
+this to ``false`` and run one Spack at a time, but otherwise we recommend
+enabling locks.
+
+--------------------
``dirty``
--------------------
diff --git a/lib/spack/llnl/util/lock.py b/lib/spack/llnl/util/lock.py
index ed797d2ebf..0d072418cc 100644
--- a/lib/spack/llnl/util/lock.py
+++ b/lib/spack/llnl/util/lock.py
@@ -195,11 +195,13 @@ class Lock(object):
"""
if self._reads == 0 and self._writes == 0:
- tty.debug('READ LOCK: {0.path}[{0._start}:{0._length}] [Acquiring]'
- .format(self))
+ self._debug(
+ 'READ LOCK: {0.path}[{0._start}:{0._length}] [Acquiring]'
+ .format(self))
self._lock(fcntl.LOCK_SH, timeout=timeout) # can raise LockError.
- tty.debug('READ LOCK: {0.path}[{0._start}:{0._length}] [Acquired]'
- .format(self))
+ self._debug(
+ 'READ LOCK: {0.path}[{0._start}:{0._length}] [Acquired]'
+ .format(self))
self._reads += 1
return True
else:
@@ -218,12 +220,13 @@ class Lock(object):
"""
if self._writes == 0:
- tty.debug(
+ self._debug(
'WRITE LOCK: {0.path}[{0._start}:{0._length}] [Acquiring]'
.format(self))
self._lock(fcntl.LOCK_EX, timeout=timeout) # can raise LockError.
- tty.debug('WRITE LOCK: {0.path}[{0._start}:{0._length}] [Acquired]'
- .format(self))
+ self._debug(
+ 'WRITE LOCK: {0.path}[{0._start}:{0._length}] [Acquired]'
+ .format(self))
self._writes += 1
return True
else:
@@ -243,8 +246,9 @@ class Lock(object):
assert self._reads > 0
if self._reads == 1 and self._writes == 0:
- tty.debug('READ LOCK: {0.path}[{0._start}:{0._length}] [Released]'
- .format(self))
+ self._debug(
+ 'READ LOCK: {0.path}[{0._start}:{0._length}] [Released]'
+ .format(self))
self._unlock() # can raise LockError.
self._reads -= 1
return True
@@ -265,8 +269,9 @@ class Lock(object):
assert self._writes > 0
if self._writes == 1 and self._reads == 0:
- tty.debug('WRITE LOCK: {0.path}[{0._start}:{0._length}] [Released]'
- .format(self))
+ self._debug(
+ 'WRITE LOCK: {0.path}[{0._start}:{0._length}] [Released]'
+ .format(self))
self._unlock() # can raise LockError.
self._writes -= 1
return True
@@ -274,6 +279,9 @@ class Lock(object):
self._writes -= 1
return False
+ def _debug(self, *args):
+ tty.debug(*args)
+
class LockTransaction(object):
"""Simple nested transaction context manager that uses a file lock.
diff --git a/lib/spack/spack/database.py b/lib/spack/spack/database.py
index 3c62cc588f..4bc6891ec8 100644
--- a/lib/spack/spack/database.py
+++ b/lib/spack/spack/database.py
@@ -52,7 +52,6 @@ from yaml.error import MarkedYAMLError, YAMLError
import llnl.util.tty as tty
from llnl.util.filesystem import mkdirp
-from llnl.util.lock import Lock, WriteTransaction, ReadTransaction
import spack.store
import spack.repo
@@ -63,6 +62,7 @@ from spack.util.crypto import bit_length
from spack.directory_layout import DirectoryLayoutError
from spack.error import SpackError
from spack.version import Version
+from spack.util.lock import Lock, WriteTransaction, ReadTransaction
# DB goes in this directory underneath the root
diff --git a/lib/spack/spack/main.py b/lib/spack/spack/main.py
index 740367168f..7af096c821 100644
--- a/lib/spack/spack/main.py
+++ b/lib/spack/spack/main.py
@@ -250,7 +250,7 @@ class SpackArgumentParser(argparse.ArgumentParser):
# epilog
formatter.add_text("""\
{help}:
- spack help --all list all available commands
+ spack help --all list all commands and options
spack help <command> help on a specific command
spack help --spec help on the spec syntax
spack docs open http://spack.rtfd.io/ in a browser"""
@@ -311,33 +311,50 @@ def make_argument_parser(**kwargs):
# stat names in groups of 7, for nice wrapping.
stat_lines = list(zip(*(iter(stat_names),) * 7))
- parser.add_argument('-h', '--help', action='store_true',
- help="show this help message and exit")
- parser.add_argument('--color', action='store', default='auto',
- choices=('always', 'never', 'auto'),
- help="when to colorize output (default: auto)")
- parser.add_argument('-d', '--debug', action='store_true',
- help="write out debug logs during compile")
- parser.add_argument('-D', '--pdb', action='store_true',
- help="run spack under the pdb debugger")
- parser.add_argument('-k', '--insecure', action='store_true',
- help="do not check ssl certificates when downloading")
- parser.add_argument('-m', '--mock', action='store_true',
- help="use mock packages instead of real ones")
- parser.add_argument('-p', '--profile', action='store_true',
- dest='spack_profile',
- help="profile execution using cProfile")
- parser.add_argument('-P', '--sorted-profile', default=None, metavar="STAT",
- help="profile and sort by one or more of:\n[%s]" %
- ',\n '.join([', '.join(line) for line in stat_lines]))
- parser.add_argument('--lines', default=20, action='store',
- help="lines of profile output or 'all' (default: 20)")
- parser.add_argument('-v', '--verbose', action='store_true',
- help="print additional output during builds")
- parser.add_argument('-s', '--stacktrace', action='store_true',
- help="add stacktraces to all printed statements")
- parser.add_argument('-V', '--version', action='store_true',
- help='show version number and exit')
+ parser.add_argument(
+ '-h', '--help', action='store_true',
+ help="show this help message and exit")
+ parser.add_argument(
+ '--color', action='store', default='auto',
+ choices=('always', 'never', 'auto'),
+ help="when to colorize output (default: auto)")
+ parser.add_argument(
+ '-d', '--debug', action='store_true',
+ help="write out debug logs during compile")
+ parser.add_argument(
+ '-D', '--pdb', action='store_true',
+ help="run spack under the pdb debugger")
+ parser.add_argument(
+ '-k', '--insecure', action='store_true',
+ help="do not check ssl certificates when downloading")
+ parser.add_argument(
+ '-l', '--enable-locks', action='store_true', dest='locks',
+ default=None, help="use filesystem locking (default)")
+ parser.add_argument(
+ '-L', '--disable-locks', action='store_false', dest='locks',
+ help="do not use filesystem locking (unsafe)")
+ parser.add_argument(
+ '-m', '--mock', action='store_true',
+ help="use mock packages instead of real ones")
+ parser.add_argument(
+ '-p', '--profile', action='store_true', dest='spack_profile',
+ help="profile execution using cProfile")
+ parser.add_argument(
+ '-P', '--sorted-profile', default=None, metavar="STAT",
+ help="profile and sort by one or more of:\n[%s]" %
+ ',\n '.join([', '.join(line) for line in stat_lines]))
+ parser.add_argument(
+ '--lines', default=20, action='store',
+ help="lines of profile output or 'all' (default: 20)")
+ parser.add_argument(
+ '-v', '--verbose', action='store_true',
+ help="print additional output during builds")
+ parser.add_argument(
+ '-s', '--stacktrace', action='store_true',
+ help="add stacktraces to all printed statements")
+ parser.add_argument(
+ '-V', '--version', action='store_true',
+ help='show version number and exit')
return parser
@@ -348,6 +365,11 @@ def setup_main_options(args):
tty.set_debug(args.debug)
tty.set_stacktrace(args.stacktrace)
+ # override lock configuration if passed on command line
+ if args.locks is not None:
+ spack.util.lock.check_lock_safety(spack.paths.prefix)
+ spack.config.set('config:locks', False, scope='command_line')
+
if args.debug:
spack.util.debug.register_interrupt_handler()
spack.config.set('config:debug', True, scope='command_line')
diff --git a/lib/spack/spack/schema/config.py b/lib/spack/spack/schema/config.py
index d38e31cfd2..4a5b25ed63 100644
--- a/lib/spack/spack/schema/config.py
+++ b/lib/spack/spack/schema/config.py
@@ -66,6 +66,7 @@ schema = {
'verify_ssl': {'type': 'boolean'},
'debug': {'type': 'boolean'},
'checksum': {'type': 'boolean'},
+ 'locks': {'type': 'boolean'},
'dirty': {'type': 'boolean'},
'build_jobs': {'type': 'integer', 'minimum': 1},
}
diff --git a/lib/spack/spack/stage.py b/lib/spack/spack/stage.py
index 0c460ddf5f..0c441b3269 100644
--- a/lib/spack/spack/stage.py
+++ b/lib/spack/spack/stage.py
@@ -34,7 +34,6 @@ from six import iteritems
from six.moves.urllib.parse import urljoin
import llnl.util.tty as tty
-import llnl.util.lock
from llnl.util.filesystem import mkdirp, can_access
from llnl.util.filesystem import remove_if_dead_link, remove_linked_tree
@@ -42,6 +41,7 @@ import spack.paths
import spack.caches
import spack.config
import spack.error
+import spack.util.lock
import spack.fetch_strategy as fs
import spack.util.pattern as pattern
from spack.util.path import canonicalize_path
@@ -231,7 +231,7 @@ class Stage(object):
lock_id = prefix_bits(sha1, bit_length(sys.maxsize))
stage_lock_path = os.path.join(spack.paths.stage_path, '.lock')
- Stage.stage_locks[self.name] = llnl.util.lock.Lock(
+ Stage.stage_locks[self.name] = spack.util.lock.Lock(
stage_lock_path, lock_id, 1)
self._lock = Stage.stage_locks[self.name]
diff --git a/lib/spack/spack/test/llnl/util/lock.py b/lib/spack/spack/test/llnl/util/lock.py
index c84573e75e..c279d7e918 100644
--- a/lib/spack/spack/test/llnl/util/lock.py
+++ b/lib/spack/spack/test/llnl/util/lock.py
@@ -73,8 +73,11 @@ from multiprocessing import Process
import pytest
from llnl.util.filesystem import touch
+
+import spack.util.lock
+from spack.util.executable import which
from spack.util.multiproc import Barrier
-from llnl.util.lock import Lock, WriteTransaction, ReadTransaction, LockError
+from spack.util.lock import Lock, WriteTransaction, ReadTransaction, LockError
#
@@ -183,15 +186,23 @@ def private_lock_path(lock_dir):
lock_file = os.path.join(lock_dir, 'lockfile')
if mpi:
lock_file += '.%s' % comm.rank
+
yield lock_file
+ if os.path.exists(lock_file):
+ os.unlink(lock_file)
+
@pytest.fixture
def lock_path(lock_dir):
"""This lock is shared among all processes in a multiproc test."""
lock_file = os.path.join(lock_dir, 'lockfile')
+
yield lock_file
+ if os.path.exists(lock_file):
+ os.unlink(lock_file)
+
def local_multiproc_test(*functions):
"""Order some processes using simple barrier synchronization."""
@@ -900,3 +911,95 @@ def test_transaction_with_context_manager_and_exception(lock_path):
assert vals['exception']
assert not vals['exited_fn']
assert not vals['exception_fn']
+
+
+def test_disable_locking(private_lock_path):
+ """Ensure that locks do no real locking when disabled."""
+ old_value = spack.config.get('config:locks')
+
+ with spack.config.override('config:locks', False):
+ lock = Lock(private_lock_path)
+
+ lock.acquire_read()
+ assert not os.path.exists(private_lock_path)
+
+ lock.acquire_write()
+ assert not os.path.exists(private_lock_path)
+
+ lock.release_write()
+ assert not os.path.exists(private_lock_path)
+
+ lock.release_read()
+ assert not os.path.exists(private_lock_path)
+
+ assert old_value == spack.config.get('config:locks')
+
+
+def test_lock_checks_user(tmpdir):
+ """Ensure lock checks work."""
+ path = str(tmpdir)
+ uid = os.getuid()
+
+ # self-owned, own group
+ tmpdir.chown(uid, uid)
+
+ # safe
+ tmpdir.chmod(0o744)
+ spack.util.lock.check_lock_safety(path)
+
+ # safe
+ tmpdir.chmod(0o774)
+ spack.util.lock.check_lock_safety(path)
+
+ # unsafe
+ tmpdir.chmod(0o777)
+ with pytest.raises(spack.error.SpackError):
+ spack.util.lock.check_lock_safety(path)
+
+ # safe
+ tmpdir.chmod(0o474)
+ spack.util.lock.check_lock_safety(path)
+
+ # safe
+ tmpdir.chmod(0o477)
+ spack.util.lock.check_lock_safety(path)
+
+
+def test_lock_checks_group(tmpdir):
+ path = str(tmpdir)
+ uid = os.getuid()
+
+ id_cmd = which('id')
+ if not id_cmd:
+ pytest.skip("Can't determine user's groups.")
+
+ # find a legal gid to user that is NOT the user's uid
+ gids = [int(gid) for gid in id_cmd('-G', output=str).split(' ')]
+ gid = next((g for g in gids if g != uid), None)
+ if gid is None:
+ pytest.skip("Can't determine user's groups.")
+
+ # self-owned, another group
+ tmpdir.chown(uid, gid)
+
+ # safe
+ tmpdir.chmod(0o744)
+ spack.util.lock.check_lock_safety(path)
+
+ # unsafe
+ tmpdir.chmod(0o774)
+ with pytest.raises(spack.error.SpackError):
+ spack.util.lock.check_lock_safety(path)
+
+ # unsafe
+ tmpdir.chmod(0o777)
+ with pytest.raises(spack.error.SpackError):
+ spack.util.lock.check_lock_safety(path)
+
+ # safe
+ tmpdir.chmod(0o474)
+ spack.util.lock.check_lock_safety(path)
+
+ # safe
+ tmpdir.chmod(0o477)
+ spack.util.lock.check_lock_safety(path)
diff --git a/lib/spack/spack/util/file_cache.py b/lib/spack/spack/util/file_cache.py
index d0551faff9..56217e3a7c 100644
--- a/lib/spack/spack/util/file_cache.py
+++ b/lib/spack/spack/util/file_cache.py
@@ -26,9 +26,9 @@ import os
import shutil
from llnl.util.filesystem import mkdirp
-from llnl.util.lock import Lock, ReadTransaction, WriteTransaction
from spack.error import SpackError
+from spack.util.lock import Lock, ReadTransaction, WriteTransaction
class FileCache(object):
diff --git a/lib/spack/spack/util/lock.py b/lib/spack/spack/util/lock.py
new file mode 100644
index 0000000000..a36cf5876d
--- /dev/null
+++ b/lib/spack/spack/util/lock.py
@@ -0,0 +1,91 @@
+##############################################################################
+# Copyright (c) 2013-2018, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://github.com/spack/spack
+# Please also see the NOTICE and LICENSE files for our notice and the LGPL.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License (as
+# published by the Free Software Foundation) version 2.1, February 1999.
+#
+# 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 terms and
+# conditions of the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser 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
+##############################################################################
+"""Wrapper for ``llnl.util.lock`` allows locking to be enabled/disabled."""
+import os
+import stat
+
+import llnl.util.lock
+from llnl.util.lock import * # noqa
+
+import spack.config
+import spack.error
+import spack.paths
+
+
+class Lock(llnl.util.lock.Lock):
+ """Lock that can be disabled.
+
+ This overrides the ``_lock()`` and ``_unlock()`` methods from
+ ``llnl.util.lock`` so that all the lock API calls will succeed, but
+ the actual locking mechanism can be disabled via ``_enable_locks``.
+ """
+ def __init__(self, *args, **kwargs):
+ super(Lock, self).__init__(*args, **kwargs)
+ self._enable = spack.config.get('config:locks', True)
+
+ def _lock(self, op, timeout=0):
+ if self._enable:
+ super(Lock, self)._lock(op, timeout)
+
+ def _unlock(self):
+ """Unlock call that always succeeds."""
+ if self._enable:
+ super(Lock, self)._unlock()
+
+ def _debug(self, *args):
+ if self._enable:
+ super(Lock, self)._debug(*args)
+
+
+def check_lock_safety(path):
+ """Do some extra checks to ensure disabling locks is safe.
+
+ This will raise an error if ``path`` can is group- or world-writable
+ AND the current user can write to the directory (i.e., if this user
+ AND others could write to the path).
+
+ This is intended to run on the Spack prefix, but can be run on any
+ path for testing.
+ """
+ if os.access(path, os.W_OK):
+ stat_result = os.stat(path)
+ uid, gid = stat_result.st_uid, stat_result.st_gid
+ mode = stat_result[stat.ST_MODE]
+
+ writable = None
+ if (mode & stat.S_IWGRP) and (uid != gid):
+ # spack is group-writeable and the group is not the owner
+ writable = 'group'
+ elif (mode & stat.S_IWOTH):
+ # spack is world-writeable
+ writable = 'world'
+
+ if writable:
+ msg = "Refusing to disable locks: spack is {0}-writable.".format(
+ writable)
+ long_msg = (
+ "Running a shared spack without locks is unsafe. You must "
+ "restrict permissions on {0} or enable locks.").format(path)
+ raise spack.error.SpackError(msg, long_msg)