Streamline key management for build caches (#17792)
* Rework spack.util.web.list_url() list_url() now accepts an optional recursive argument (default: False) for controlling whether to only return files within the prefix url or to return all files whose path starts with the prefix url. Allows for the most effecient implementation for the given prefix url scheme. For example, only recursive queries are supported for S3 prefixes, so the returned list is trimmed down if recursive == False, but the native search is returned as-is when recursive == True. Suitable implementations for each case are also used for file system URLs. * Switch to using an explicit index for public keys Switches to maintaining a build cache's keys under build_cache/_pgp. Within this directory is an index.json file listing all the available keys and a <fingerprint>.pub file for each such key. - Adds spack.binary_distribution.generate_key_index() - (re)generates a build cache's key index - Modifies spack.binary_distribution.build_tarball() - if tarball is signed, automatically pushes the key used for signing along with the tarball - if regenerate_index == True, automatically (re)generates the build cache's key index along with the build cache's package index; as in spack.binary_distribution.generate_key_index() - Modifies spack.binary_distribution.get_keys() - a build cache's key index is now used instead of programmatic listing - Adds spack.binary_distribution.push_keys() - publishes keys from Spack's keyring to a given list of mirrors - Adds new spack subcommand: spack gpg publish - publishes keys from Spack's keyring to a given list of mirrors - Modifies spack.util.gpg.Gpg.signing_keys() - Accepts optional positional arguments for filtering the set of keys returned - Adds spack.util.gpg.Gpg.public_keys() - As spack.util.gpg.Gpg.signing_keys(), except public keys are returned - Modifies spack.util.gpg.Gpg.export_keys() - Fixes an issue where GnuPG would prompt for user input if trying to overwrite an existing file - Modifies spack.util.gpg.Gpg.untrust() - Fixes an issue where GnuPG would fail for input that were not key fingerprints - Modifies spack.util.web.url_exists() - Fixes an issue where url_exists() would throw instead of returning False * rework gpg module/fix error with very long GNUPGHOME dir * add a shim for functools.cached_property * handle permission denied error in gpg util * fix tests/make gpgconf optional if no socket dir is available
This commit is contained in:
parent
421f4e12a7
commit
2d93154119
14 changed files with 734 additions and 214 deletions
|
@ -26,17 +26,18 @@
|
|||
import spack.config as config
|
||||
import spack.database as spack_db
|
||||
import spack.fetch_strategy as fs
|
||||
import spack.util.gpg
|
||||
import spack.relocate as relocate
|
||||
import spack.util.gpg
|
||||
import spack.util.spack_json as sjson
|
||||
import spack.util.spack_yaml as syaml
|
||||
import spack.mirror
|
||||
import spack.util.url as url_util
|
||||
import spack.util.web as web_util
|
||||
from spack.spec import Spec
|
||||
from spack.stage import Stage
|
||||
from spack.util.gpg import Gpg
|
||||
|
||||
_build_cache_relative_path = 'build_cache'
|
||||
_build_cache_keys_relative_path = '_pgp'
|
||||
|
||||
BUILD_CACHE_INDEX_TEMPLATE = '''
|
||||
<html>
|
||||
|
@ -247,15 +248,9 @@ def checksum_tarball(file):
|
|||
return hasher.hexdigest()
|
||||
|
||||
|
||||
def sign_tarball(key, force, specfile_path):
|
||||
# Sign the packages if keys available
|
||||
if spack.util.gpg.Gpg.gpg() is None:
|
||||
raise NoGpgException(
|
||||
"gpg2 is not available in $PATH .\n"
|
||||
"Use spack install gnupg and spack load gnupg.")
|
||||
|
||||
def select_signing_key(key=None):
|
||||
if key is None:
|
||||
keys = Gpg.signing_keys()
|
||||
keys = spack.util.gpg.signing_keys()
|
||||
if len(keys) == 1:
|
||||
key = keys[0]
|
||||
|
||||
|
@ -263,26 +258,30 @@ def sign_tarball(key, force, specfile_path):
|
|||
raise PickKeyException(str(keys))
|
||||
|
||||
if len(keys) == 0:
|
||||
msg = "No default key available for signing.\n"
|
||||
msg += "Use spack gpg init and spack gpg create"
|
||||
msg += " to create a default key."
|
||||
raise NoKeyException(msg)
|
||||
raise NoKeyException(
|
||||
"No default key available for signing.\n"
|
||||
"Use spack gpg init and spack gpg create"
|
||||
" to create a default key.")
|
||||
return key
|
||||
|
||||
|
||||
def sign_tarball(key, force, specfile_path):
|
||||
if os.path.exists('%s.asc' % specfile_path):
|
||||
if force:
|
||||
os.remove('%s.asc' % specfile_path)
|
||||
else:
|
||||
raise NoOverwriteException('%s.asc' % specfile_path)
|
||||
|
||||
Gpg.sign(key, specfile_path, '%s.asc' % specfile_path)
|
||||
key = select_signing_key(key)
|
||||
spack.util.gpg.sign(key, specfile_path, '%s.asc' % specfile_path)
|
||||
|
||||
|
||||
def generate_package_index(cache_prefix):
|
||||
"""Create the build cache index page.
|
||||
|
||||
Creates (or replaces) the "index.json" page at the location given in
|
||||
cache_prefix. This page contains a link for each binary package (*.yaml)
|
||||
and public key (*.key) under cache_prefix.
|
||||
cache_prefix. This page contains a link for each binary package (.yaml)
|
||||
under cache_prefix.
|
||||
"""
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
db_root_dir = os.path.join(tmpdir, 'db_root')
|
||||
|
@ -325,6 +324,45 @@ def generate_package_index(cache_prefix):
|
|||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def generate_key_index(key_prefix, tmpdir=None):
|
||||
"""Create the key index page.
|
||||
|
||||
Creates (or replaces) the "index.json" page at the location given in
|
||||
key_prefix. This page contains an entry for each key (.pub) under
|
||||
key_prefix.
|
||||
"""
|
||||
|
||||
tty.debug(' '.join(('Retrieving key.pub files from',
|
||||
url_util.format(key_prefix),
|
||||
'to build key index')))
|
||||
|
||||
fingerprints = (
|
||||
entry[:-4]
|
||||
for entry in web_util.list_url(key_prefix, recursive=False)
|
||||
if entry.endswith('.pub'))
|
||||
|
||||
keys_local = url_util.local_file_path(key_prefix)
|
||||
if keys_local:
|
||||
target = os.path.join(keys_local, 'index.json')
|
||||
else:
|
||||
target = os.path.join(tmpdir, 'index.json')
|
||||
|
||||
index = {
|
||||
'keys': dict(
|
||||
(fingerprint, {}) for fingerprint
|
||||
in sorted(set(fingerprints)))
|
||||
}
|
||||
with open(target, 'w') as f:
|
||||
sjson.dump(index, f)
|
||||
|
||||
if not keys_local:
|
||||
web_util.push_to_url(
|
||||
target,
|
||||
url_util.join(key_prefix, 'index.json'),
|
||||
keep_original=False,
|
||||
extra_args={'ContentType': 'application/json'})
|
||||
|
||||
|
||||
def build_tarball(spec, outdir, force=False, rel=False, unsigned=False,
|
||||
allow_root=False, key=None, regenerate_index=False):
|
||||
"""
|
||||
|
@ -445,7 +483,9 @@ def build_tarball(spec, outdir, force=False, rel=False, unsigned=False,
|
|||
|
||||
# sign the tarball and spec file with gpg
|
||||
if not unsigned:
|
||||
key = select_signing_key(key)
|
||||
sign_tarball(key, force, specfile_path)
|
||||
|
||||
# put tarball, spec and signature files in .spack archive
|
||||
with closing(tarfile.open(spackfile_path, 'w')) as tar:
|
||||
tar.add(name=tarfile_path, arcname='%s' % tarfile_name)
|
||||
|
@ -468,7 +508,15 @@ def build_tarball(spec, outdir, force=False, rel=False, unsigned=False,
|
|||
.format(spec, remote_spackfile_path))
|
||||
|
||||
try:
|
||||
# create an index.html for the build_cache directory so specs can be
|
||||
# push the key to the build cache's _pgp directory so it can be
|
||||
# imported
|
||||
if not unsigned:
|
||||
push_keys(outdir,
|
||||
keys=[key],
|
||||
regenerate_index=regenerate_index,
|
||||
tmpdir=tmpdir)
|
||||
|
||||
# create an index.json for the build_cache directory so specs can be
|
||||
# found
|
||||
if regenerate_index:
|
||||
generate_package_index(url_util.join(
|
||||
|
@ -695,7 +743,8 @@ def extract_tarball(spec, filename, allow_root=False, unsigned=False,
|
|||
if os.path.exists('%s.asc' % specfile_path):
|
||||
try:
|
||||
suppress = config.get('config:suppress_gpg_warnings', False)
|
||||
Gpg.verify('%s.asc' % specfile_path, specfile_path, suppress)
|
||||
spack.util.gpg.verify(
|
||||
'%s.asc' % specfile_path, specfile_path, suppress)
|
||||
except Exception as e:
|
||||
shutil.rmtree(tmpdir)
|
||||
raise e
|
||||
|
@ -898,41 +947,46 @@ def get_specs():
|
|||
return _cached_specs
|
||||
|
||||
|
||||
def get_keys(install=False, trust=False, force=False):
|
||||
def get_keys(install=False, trust=False, force=False, mirrors=None):
|
||||
"""Get pgp public keys available on mirror with suffix .pub
|
||||
"""
|
||||
Get pgp public keys available on mirror
|
||||
with suffix .key or .pub
|
||||
"""
|
||||
if not spack.mirror.MirrorCollection():
|
||||
mirror_collection = (mirrors or spack.mirror.MirrorCollection())
|
||||
|
||||
if not mirror_collection:
|
||||
tty.die("Please add a spack mirror to allow " +
|
||||
"download of build caches.")
|
||||
|
||||
keys = set()
|
||||
for mirror in mirror_collection.values():
|
||||
fetch_url = mirror.fetch_url
|
||||
keys_url = url_util.join(fetch_url,
|
||||
_build_cache_relative_path,
|
||||
_build_cache_keys_relative_path)
|
||||
keys_index = url_util.join(keys_url, 'index.json')
|
||||
|
||||
for mirror in spack.mirror.MirrorCollection().values():
|
||||
fetch_url_build_cache = url_util.join(
|
||||
mirror.fetch_url, _build_cache_relative_path)
|
||||
tty.debug('Finding public keys in {0}'.format(
|
||||
url_util.format(fetch_url)))
|
||||
|
||||
mirror_dir = url_util.local_file_path(fetch_url_build_cache)
|
||||
if mirror_dir:
|
||||
tty.debug('Finding public keys in {0}'.format(mirror_dir))
|
||||
files = os.listdir(str(mirror_dir))
|
||||
for file in files:
|
||||
if re.search(r'\.key', file) or re.search(r'\.pub', file):
|
||||
link = url_util.join(fetch_url_build_cache, file)
|
||||
keys.add(link)
|
||||
else:
|
||||
tty.debug('Finding public keys at {0}'
|
||||
.format(url_util.format(fetch_url_build_cache)))
|
||||
# For s3 mirror need to request index.html directly
|
||||
p, links = web_util.spider(
|
||||
url_util.join(fetch_url_build_cache, 'index.html'))
|
||||
try:
|
||||
_, _, json_file = web_util.read_from_url(keys_index)
|
||||
json_index = sjson.load(codecs.getreader('utf-8')(json_file))
|
||||
except (URLError, web_util.SpackWebError) as url_err:
|
||||
if web_util.url_exists(keys_index):
|
||||
err_msg = [
|
||||
'Unable to find public keys in {0},',
|
||||
' caught exception attempting to read from {1}.',
|
||||
]
|
||||
|
||||
for link in links:
|
||||
if re.search(r'\.key', link) or re.search(r'\.pub', link):
|
||||
keys.add(link)
|
||||
tty.error(''.join(err_msg).format(
|
||||
url_util.format(fetch_url),
|
||||
url_util.format(keys_index)))
|
||||
|
||||
tty.debug(url_err)
|
||||
|
||||
continue
|
||||
|
||||
for fingerprint, key_attributes in json_index['keys'].items():
|
||||
link = os.path.join(keys_url, fingerprint + '.pub')
|
||||
|
||||
for link in keys:
|
||||
with Stage(link, name="build_cache", keep=True) as stage:
|
||||
if os.path.exists(stage.save_filename) and force:
|
||||
os.remove(stage.save_filename)
|
||||
|
@ -941,16 +995,80 @@ def get_keys(install=False, trust=False, force=False):
|
|||
stage.fetch()
|
||||
except fs.FetchError:
|
||||
continue
|
||||
tty.debug('Found key {0}'.format(link))
|
||||
|
||||
tty.debug('Found key {0}'.format(fingerprint))
|
||||
if install:
|
||||
if trust:
|
||||
Gpg.trust(stage.save_filename)
|
||||
spack.util.gpg.trust(stage.save_filename)
|
||||
tty.debug('Added this key to trusted keys.')
|
||||
else:
|
||||
tty.debug('Will not add this key to trusted keys.'
|
||||
'Use -t to install all downloaded keys')
|
||||
|
||||
|
||||
def push_keys(*mirrors, **kwargs):
|
||||
"""
|
||||
Upload pgp public keys to the given mirrors
|
||||
"""
|
||||
keys = kwargs.get('keys')
|
||||
regenerate_index = kwargs.get('regenerate_index', False)
|
||||
tmpdir = kwargs.get('tmpdir')
|
||||
remove_tmpdir = False
|
||||
|
||||
keys = spack.util.gpg.public_keys(*(keys or []))
|
||||
|
||||
try:
|
||||
for mirror in mirrors:
|
||||
push_url = getattr(mirror, 'push_url', mirror)
|
||||
keys_url = url_util.join(push_url,
|
||||
_build_cache_relative_path,
|
||||
_build_cache_keys_relative_path)
|
||||
keys_local = url_util.local_file_path(keys_url)
|
||||
|
||||
verb = 'Writing' if keys_local else 'Uploading'
|
||||
tty.debug('{0} public keys to {1}'.format(
|
||||
verb, url_util.format(push_url)))
|
||||
|
||||
if keys_local: # mirror is local, don't bother with the tmpdir
|
||||
prefix = keys_local
|
||||
mkdirp(keys_local)
|
||||
else:
|
||||
# A tmp dir is created for the first mirror that is non-local.
|
||||
# On the off-hand chance that all the mirrors are local, then
|
||||
# we can avoid the need to create a tmp dir.
|
||||
if tmpdir is None:
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
remove_tmpdir = True
|
||||
prefix = tmpdir
|
||||
|
||||
for fingerprint in keys:
|
||||
tty.debug(' ' + fingerprint)
|
||||
filename = fingerprint + '.pub'
|
||||
|
||||
export_target = os.path.join(prefix, filename)
|
||||
spack.util.gpg.export_keys(export_target, fingerprint)
|
||||
|
||||
# If mirror is local, the above export writes directly to the
|
||||
# mirror (export_target points directly to the mirror).
|
||||
#
|
||||
# If not, then export_target is a tmpfile that needs to be
|
||||
# uploaded to the mirror.
|
||||
if not keys_local:
|
||||
spack.util.web.push_to_url(
|
||||
export_target,
|
||||
url_util.join(keys_url, filename),
|
||||
keep_original=False)
|
||||
|
||||
if regenerate_index:
|
||||
if keys_local:
|
||||
generate_key_index(keys_url)
|
||||
else:
|
||||
generate_key_index(keys_url, tmpdir)
|
||||
finally:
|
||||
if remove_tmpdir:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def needs_rebuild(spec, mirror_url, rebuild_on_errors=False):
|
||||
if not spec.concrete:
|
||||
raise ValueError('spec must be concrete to check against mirror')
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
import os
|
||||
import argparse
|
||||
|
||||
import spack.binary_distribution
|
||||
import spack.cmd.common.arguments as arguments
|
||||
import spack.paths
|
||||
from spack.util.gpg import Gpg
|
||||
import spack.util.gpg
|
||||
|
||||
description = "handle GPG actions for spack"
|
||||
section = "packaging"
|
||||
|
@ -81,37 +82,64 @@ def setup_parser(subparser):
|
|||
'all secret keys if unspecified')
|
||||
export.set_defaults(func=gpg_export)
|
||||
|
||||
publish = subparsers.add_parser('publish', help=gpg_publish.__doc__)
|
||||
|
||||
output = publish.add_mutually_exclusive_group(required=True)
|
||||
output.add_argument('-d', '--directory',
|
||||
metavar='directory',
|
||||
type=str,
|
||||
help="local directory where " +
|
||||
"keys will be published.")
|
||||
output.add_argument('-m', '--mirror-name',
|
||||
metavar='mirror-name',
|
||||
type=str,
|
||||
help="name of the mirror where " +
|
||||
"keys will be published.")
|
||||
output.add_argument('--mirror-url',
|
||||
metavar='mirror-url',
|
||||
type=str,
|
||||
help="URL of the mirror where " +
|
||||
"keys will be published.")
|
||||
publish.add_argument('--rebuild-index', action='store_true',
|
||||
default=False, help=(
|
||||
"Regenerate buildcache key index "
|
||||
"after publishing key(s)"))
|
||||
publish.add_argument('keys', nargs='*',
|
||||
help='the keys to publish; '
|
||||
'all public keys if unspecified')
|
||||
publish.set_defaults(func=gpg_publish)
|
||||
|
||||
|
||||
def gpg_create(args):
|
||||
"""create a new key"""
|
||||
if args.export:
|
||||
old_sec_keys = Gpg.signing_keys()
|
||||
Gpg.create(name=args.name, email=args.email,
|
||||
comment=args.comment, expires=args.expires)
|
||||
old_sec_keys = spack.util.gpg.signing_keys()
|
||||
spack.util.gpg.create(name=args.name, email=args.email,
|
||||
comment=args.comment, expires=args.expires)
|
||||
if args.export:
|
||||
new_sec_keys = set(Gpg.signing_keys())
|
||||
new_sec_keys = set(spack.util.gpg.signing_keys())
|
||||
new_keys = new_sec_keys.difference(old_sec_keys)
|
||||
Gpg.export_keys(args.export, *new_keys)
|
||||
spack.util.gpg.export_keys(args.export, *new_keys)
|
||||
|
||||
|
||||
def gpg_export(args):
|
||||
"""export a secret key"""
|
||||
keys = args.keys
|
||||
if not keys:
|
||||
keys = Gpg.signing_keys()
|
||||
Gpg.export_keys(args.location, *keys)
|
||||
keys = spack.util.gpg.signing_keys()
|
||||
spack.util.gpg.export_keys(args.location, *keys)
|
||||
|
||||
|
||||
def gpg_list(args):
|
||||
"""list keys available in the keyring"""
|
||||
Gpg.list(args.trusted, args.signing)
|
||||
spack.util.gpg.list(args.trusted, args.signing)
|
||||
|
||||
|
||||
def gpg_sign(args):
|
||||
"""sign a package"""
|
||||
key = args.key
|
||||
if key is None:
|
||||
keys = Gpg.signing_keys()
|
||||
keys = spack.util.gpg.signing_keys()
|
||||
if len(keys) == 1:
|
||||
key = keys[0]
|
||||
elif not keys:
|
||||
|
@ -123,12 +151,12 @@ def gpg_sign(args):
|
|||
if not output:
|
||||
output = args.spec[0] + '.asc'
|
||||
# TODO: Support the package format Spack creates.
|
||||
Gpg.sign(key, ' '.join(args.spec), output, args.clearsign)
|
||||
spack.util.gpg.sign(key, ' '.join(args.spec), output, args.clearsign)
|
||||
|
||||
|
||||
def gpg_trust(args):
|
||||
"""add a key to the keyring"""
|
||||
Gpg.trust(args.keyfile)
|
||||
spack.util.gpg.trust(args.keyfile)
|
||||
|
||||
|
||||
def gpg_init(args):
|
||||
|
@ -141,12 +169,12 @@ def gpg_init(args):
|
|||
for filename in filenames:
|
||||
if not filename.endswith('.key'):
|
||||
continue
|
||||
Gpg.trust(os.path.join(root, filename))
|
||||
spack.util.gpg.trust(os.path.join(root, filename))
|
||||
|
||||
|
||||
def gpg_untrust(args):
|
||||
"""remove a key from the keyring"""
|
||||
Gpg.untrust(args.signing, *args.keys)
|
||||
spack.util.gpg.untrust(args.signing, *args.keys)
|
||||
|
||||
|
||||
def gpg_verify(args):
|
||||
|
@ -155,7 +183,17 @@ def gpg_verify(args):
|
|||
signature = args.signature
|
||||
if signature is None:
|
||||
signature = args.spec[0] + '.asc'
|
||||
Gpg.verify(signature, ' '.join(args.spec))
|
||||
spack.util.gpg.verify(signature, ' '.join(args.spec))
|
||||
|
||||
|
||||
def gpg_publish(args):
|
||||
"""publish public keys to a build cache"""
|
||||
|
||||
# TODO(opadron): switch to using the mirror args once #17547 is merged
|
||||
mirror = args.directory
|
||||
|
||||
spack.binary_distribution.push_keys(
|
||||
mirror, keys=args.keys, regenerate_index=args.rebuild_index)
|
||||
|
||||
|
||||
def gpg(parser, args):
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
import spack.cmd.install as install
|
||||
import spack.cmd.uninstall as uninstall
|
||||
import spack.cmd.mirror as mirror
|
||||
from spack.spec import Spec
|
||||
import spack.mirror
|
||||
import spack.util.gpg
|
||||
from spack.directory_layout import YamlDirectoryLayout
|
||||
from spack.spec import Spec
|
||||
|
||||
|
||||
def_install_path_scheme = '${ARCHITECTURE}/${COMPILERNAME}-${COMPILERVER}/${PACKAGE}-${VERSION}-${HASH}' # noqa: E501
|
||||
|
@ -469,3 +471,40 @@ def test_relative_rpaths_install_nondefault(tmpdir,
|
|||
margs = mparser.parse_args(
|
||||
['rm', '--scope', 'site', 'test-mirror-rel'])
|
||||
mirror.mirror(mparser, margs)
|
||||
|
||||
|
||||
def test_push_and_fetch_keys(mock_gnupghome):
|
||||
testpath = str(mock_gnupghome)
|
||||
|
||||
mirror = os.path.join(testpath, 'mirror')
|
||||
mirrors = {'test-mirror': mirror}
|
||||
mirrors = spack.mirror.MirrorCollection(mirrors)
|
||||
mirror = spack.mirror.Mirror('file://' + mirror)
|
||||
|
||||
gpg_dir1 = os.path.join(testpath, 'gpg1')
|
||||
gpg_dir2 = os.path.join(testpath, 'gpg2')
|
||||
|
||||
# dir 1: create a new key, record its fingerprint, and push it to a new
|
||||
# mirror
|
||||
with spack.util.gpg.gnupg_home_override(gpg_dir1):
|
||||
spack.util.gpg.create(name='test-key',
|
||||
email='fake@test.key',
|
||||
expires='0',
|
||||
comment=None)
|
||||
|
||||
keys = spack.util.gpg.public_keys()
|
||||
assert len(keys) == 1
|
||||
fpr = keys[0]
|
||||
|
||||
bindist.push_keys(mirror, keys=[fpr], regenerate_index=True)
|
||||
|
||||
# dir 2: import the key from the mirror, and confirm that its fingerprint
|
||||
# matches the one created above
|
||||
with spack.util.gpg.gnupg_home_override(gpg_dir2):
|
||||
assert len(spack.util.gpg.public_keys()) == 0
|
||||
|
||||
bindist.get_keys(mirrors=mirrors, install=True, trust=True, force=True)
|
||||
|
||||
new_keys = spack.util.gpg.public_keys()
|
||||
assert len(new_keys) == 1
|
||||
assert new_keys[0] == fpr
|
||||
|
|
|
@ -53,15 +53,8 @@ def test_urlencode_string():
|
|||
assert(s_enc == 'Spack+Test+Project')
|
||||
|
||||
|
||||
def has_gpg():
|
||||
try:
|
||||
gpg = spack.util.gpg.Gpg.gpg()
|
||||
except spack.util.gpg.SpackGPGError:
|
||||
gpg = None
|
||||
return bool(gpg)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not has_gpg(), reason='This test requires gpg')
|
||||
@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
|
||||
reason='This test requires gpg')
|
||||
def test_import_signing_key(mock_gnupghome):
|
||||
signing_key_dir = spack_paths.mock_gpg_keys_path
|
||||
signing_key_path = os.path.join(signing_key_dir, 'package-signing-key')
|
||||
|
|
|
@ -35,14 +35,6 @@
|
|||
git = exe.which('git', required=True)
|
||||
|
||||
|
||||
def has_gpg():
|
||||
try:
|
||||
gpg = spack.util.gpg.Gpg.gpg()
|
||||
except spack.util.gpg.SpackGPGError:
|
||||
gpg = None
|
||||
return bool(gpg)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def env_deactivate():
|
||||
yield
|
||||
|
@ -690,7 +682,8 @@ def test_ci_rebuild_basic(tmpdir, mutable_mock_env_path, env_deactivate,
|
|||
|
||||
|
||||
@pytest.mark.disable_clean_stage_check
|
||||
@pytest.mark.skipif(not has_gpg(), reason='This test requires gpg')
|
||||
@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
|
||||
reason='This test requires gpg')
|
||||
def test_push_mirror_contents(tmpdir, mutable_mock_env_path, env_deactivate,
|
||||
install_mockery, mock_packages, mock_fetch,
|
||||
mock_stage, mock_gnupghome):
|
||||
|
|
|
@ -29,39 +29,35 @@
|
|||
('gpg2', 'gpg (GnuPG) 2.2.19'), # gpg2 command
|
||||
])
|
||||
def test_find_gpg(cmd_name, version, tmpdir, mock_gnupghome, monkeypatch):
|
||||
TEMPLATE = ('#!/bin/sh\n'
|
||||
'echo "{version}"\n')
|
||||
|
||||
with tmpdir.as_cwd():
|
||||
with open(cmd_name, 'w') as f:
|
||||
f.write("""\
|
||||
#!/bin/sh
|
||||
echo "{version}"
|
||||
""".format(version=version))
|
||||
fs.set_executable(cmd_name)
|
||||
for fname in (cmd_name, 'gpgconf'):
|
||||
with open(fname, 'w') as f:
|
||||
f.write(TEMPLATE.format(version=version))
|
||||
fs.set_executable(fname)
|
||||
|
||||
monkeypatch.setitem(os.environ, "PATH", str(tmpdir))
|
||||
if version == 'undetectable' or version.endswith('1.3.4'):
|
||||
with pytest.raises(spack.util.gpg.SpackGPGError):
|
||||
exe = spack.util.gpg.Gpg.gpg()
|
||||
spack.util.gpg.ensure_gpg(reevaluate=True)
|
||||
else:
|
||||
exe = spack.util.gpg.Gpg.gpg()
|
||||
assert isinstance(exe, spack.util.executable.Executable)
|
||||
spack.util.gpg.ensure_gpg(reevaluate=True)
|
||||
gpg_exe = spack.util.gpg.get_global_gpg_instance().gpg_exe
|
||||
assert isinstance(gpg_exe, spack.util.executable.Executable)
|
||||
gpgconf_exe = spack.util.gpg.get_global_gpg_instance().gpgconf_exe
|
||||
assert isinstance(gpgconf_exe, spack.util.executable.Executable)
|
||||
|
||||
|
||||
def test_no_gpg_in_path(tmpdir, mock_gnupghome, monkeypatch):
|
||||
monkeypatch.setitem(os.environ, "PATH", str(tmpdir))
|
||||
with pytest.raises(spack.util.gpg.SpackGPGError):
|
||||
spack.util.gpg.Gpg.gpg()
|
||||
|
||||
|
||||
def has_gpg():
|
||||
try:
|
||||
gpg = spack.util.gpg.Gpg.gpg()
|
||||
except spack.util.gpg.SpackGPGError:
|
||||
gpg = None
|
||||
return bool(gpg)
|
||||
spack.util.gpg.ensure_gpg(reevaluate=True)
|
||||
|
||||
|
||||
@pytest.mark.maybeslow
|
||||
@pytest.mark.skipif(not has_gpg(),
|
||||
@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
|
||||
reason='These tests require gnupg2')
|
||||
def test_gpg(tmpdir, mock_gnupghome):
|
||||
# Verify a file with an empty keyring.
|
||||
|
@ -103,7 +99,7 @@ def test_gpg(tmpdir, mock_gnupghome):
|
|||
'--export', str(keypath),
|
||||
'Spack testing 1',
|
||||
'spack@googlegroups.com')
|
||||
keyfp = spack.util.gpg.Gpg.signing_keys()[0]
|
||||
keyfp = spack.util.gpg.signing_keys()[0]
|
||||
|
||||
# List the keys.
|
||||
# TODO: Test the output here.
|
||||
|
|
|
@ -781,10 +781,8 @@ def mock_gnupghome(monkeypatch):
|
|||
# This comes up because tmp paths on macOS are already long-ish, and
|
||||
# pytest makes them longer.
|
||||
short_name_tmpdir = tempfile.mkdtemp()
|
||||
monkeypatch.setattr(spack.util.gpg, 'GNUPGHOME', short_name_tmpdir)
|
||||
monkeypatch.setattr(spack.util.gpg.Gpg, '_gpg', None)
|
||||
|
||||
yield
|
||||
with spack.util.gpg.gnupg_home_override(short_name_tmpdir):
|
||||
yield short_name_tmpdir
|
||||
|
||||
# clean up, since we are doing this manually
|
||||
shutil.rmtree(short_name_tmpdir)
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import spack.store
|
||||
import spack.binary_distribution as bindist
|
||||
import spack.cmd.buildcache as buildcache
|
||||
import spack.util.gpg
|
||||
from spack.spec import Spec
|
||||
from spack.paths import mock_gpg_keys_path
|
||||
from spack.fetch_strategy import URLFetchStrategy, FetchStrategyComposite
|
||||
|
@ -31,14 +32,6 @@
|
|||
from spack.relocate import file_is_relocatable
|
||||
|
||||
|
||||
def has_gpg():
|
||||
try:
|
||||
gpg = spack.util.gpg.Gpg.gpg()
|
||||
except spack.util.gpg.SpackGPGError:
|
||||
gpg = None
|
||||
return bool(gpg)
|
||||
|
||||
|
||||
def fake_fetchify(url, pkg):
|
||||
"""Fake the URL for a package so it downloads from a file."""
|
||||
fetcher = FetchStrategyComposite()
|
||||
|
@ -46,7 +39,8 @@ def fake_fetchify(url, pkg):
|
|||
pkg.fetcher = fetcher
|
||||
|
||||
|
||||
@pytest.mark.skipif(not has_gpg(), reason='This test requires gpg')
|
||||
@pytest.mark.skipif(not spack.util.gpg.has_gpg(),
|
||||
reason='This test requires gpg')
|
||||
@pytest.mark.usefixtures('install_mockery', 'mock_gnupghome')
|
||||
def test_buildcache(mock_archive, tmpdir):
|
||||
# tweak patchelf to only do a download
|
||||
|
@ -101,12 +95,9 @@ def test_buildcache(mock_archive, tmpdir):
|
|||
|
||||
create_args = ['create', '-a', '-f', '-d', mirror_path, pkghash]
|
||||
# Create a private key to sign package with if gpg2 available
|
||||
if spack.util.gpg.Gpg.gpg():
|
||||
spack.util.gpg.Gpg.create(name='test key 1', expires='0',
|
||||
email='spack@googlegroups.com',
|
||||
comment='Spack test key')
|
||||
else:
|
||||
create_args.insert(create_args.index('-a'), '-u')
|
||||
spack.util.gpg.create(name='test key 1', expires='0',
|
||||
email='spack@googlegroups.com',
|
||||
comment='Spack test key')
|
||||
|
||||
create_args.insert(create_args.index('-a'), '--rebuild-index')
|
||||
|
||||
|
@ -119,8 +110,6 @@ def test_buildcache(mock_archive, tmpdir):
|
|||
pkg.do_uninstall(force=True)
|
||||
|
||||
install_args = ['install', '-a', '-f', pkghash]
|
||||
if not spack.util.gpg.Gpg.gpg():
|
||||
install_args.insert(install_args.index('-a'), '-u')
|
||||
args = parser.parse_args(install_args)
|
||||
# Test install
|
||||
buildcache.buildcache(parser, args)
|
||||
|
@ -144,8 +133,6 @@ def test_buildcache(mock_archive, tmpdir):
|
|||
# Uninstall the package
|
||||
pkg.do_uninstall(force=True)
|
||||
|
||||
if not spack.util.gpg.Gpg.gpg():
|
||||
install_args.insert(install_args.index('-a'), '-u')
|
||||
args = parser.parse_args(install_args)
|
||||
buildcache.buildcache(parser, args)
|
||||
|
||||
|
|
|
@ -839,7 +839,8 @@ def test_get_stage_root_in_spack(self, clear_stage_root):
|
|||
assert 'spack' in path.split(os.path.sep)
|
||||
|
||||
# Make sure cached stage path value was changed appropriately
|
||||
assert spack.stage._stage_root == test_path
|
||||
assert spack.stage._stage_root in (
|
||||
test_path, os.path.join(test_path, getpass.getuser()))
|
||||
|
||||
# Make sure the directory exists
|
||||
assert os.path.isdir(spack.stage._stage_root)
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
|
||||
import spack.util.gpg as gpg
|
||||
import os
|
||||
import pytest
|
||||
|
||||
import spack.util.gpg
|
||||
|
||||
|
||||
def test_parse_gpg_output_case_one():
|
||||
|
@ -17,7 +20,7 @@ def test_parse_gpg_output_case_one():
|
|||
uid:::::::AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA::Joe (Test) <j.s@s.com>:
|
||||
ssb::2048:1:AAAAAAAAAAAAAAAA:AAAAAAAAAA::::::::::
|
||||
"""
|
||||
keys = gpg.parse_keys_output(output)
|
||||
keys = spack.util.gpg.parse_secret_keys_output(output)
|
||||
|
||||
assert len(keys) == 2
|
||||
assert keys[0] == 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
|
||||
|
@ -34,7 +37,7 @@ def test_parse_gpg_output_case_two():
|
|||
fpr:::::::::YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY:
|
||||
grp:::::::::AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:
|
||||
"""
|
||||
keys = gpg.parse_keys_output(output)
|
||||
keys = spack.util.gpg.parse_secret_keys_output(output)
|
||||
|
||||
assert len(keys) == 1
|
||||
assert keys[0] == 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
|
||||
|
@ -53,8 +56,29 @@ def test_parse_gpg_output_case_three():
|
|||
ssb::2048:1:AAAAAAAAAAAAAAAA:AAAAAAAAAA::::::::::
|
||||
fpr:::::::::ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ:"""
|
||||
|
||||
keys = gpg.parse_keys_output(output)
|
||||
keys = spack.util.gpg.parse_secret_keys_output(output)
|
||||
|
||||
assert len(keys) == 2
|
||||
assert keys[0] == 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'
|
||||
assert keys[1] == 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY'
|
||||
|
||||
|
||||
@pytest.mark.skipif(not spack.util.gpg.GpgConstants.user_run_dir,
|
||||
reason='This test requires /var/run/user/$(id -u)')
|
||||
def test_really_long_gnupg_home_dir(tmpdir):
|
||||
N = 960
|
||||
|
||||
tdir = str(tmpdir)
|
||||
while len(tdir) < N:
|
||||
tdir = os.path.join(tdir, 'filler')
|
||||
|
||||
tdir = tdir[:N].rstrip(os.sep)
|
||||
tdir += '0' * (N - len(tdir))
|
||||
|
||||
with spack.util.gpg.gnupg_home_override(tdir):
|
||||
spack.util.gpg.create(name='Spack testing 1',
|
||||
email='test@spack.io',
|
||||
comment='Spack testing key',
|
||||
expires='0')
|
||||
|
||||
spack.util.gpg.list(True, True)
|
||||
|
|
|
@ -167,3 +167,31 @@ def test_get_header():
|
|||
# If there isn't even a fuzzy match, raise KeyError
|
||||
with pytest.raises(KeyError):
|
||||
spack.util.web.get_header(headers, 'ContentLength')
|
||||
|
||||
|
||||
def test_list_url(tmpdir):
|
||||
testpath = str(tmpdir)
|
||||
|
||||
os.mkdir(os.path.join(testpath, 'dir'))
|
||||
|
||||
with open(os.path.join(testpath, 'file-0.txt'), 'w'):
|
||||
pass
|
||||
with open(os.path.join(testpath, 'file-1.txt'), 'w'):
|
||||
pass
|
||||
with open(os.path.join(testpath, 'file-2.txt'), 'w'):
|
||||
pass
|
||||
|
||||
with open(os.path.join(testpath, 'dir', 'another-file.txt'), 'w'):
|
||||
pass
|
||||
|
||||
list_url = lambda recursive: list(sorted(
|
||||
spack.util.web.list_url(testpath, recursive=recursive)))
|
||||
|
||||
assert list_url(False) == ['file-0.txt',
|
||||
'file-1.txt',
|
||||
'file-2.txt']
|
||||
|
||||
assert list_url(True) == ['dir/another-file.txt',
|
||||
'file-0.txt',
|
||||
'file-1.txt',
|
||||
'file-2.txt']
|
||||
|
|
|
@ -3,20 +3,73 @@
|
|||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
|
||||
import contextlib
|
||||
import errno
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
|
||||
import llnl.util.lang
|
||||
|
||||
import spack.error
|
||||
import spack.paths
|
||||
import spack.util.executable
|
||||
import spack.version
|
||||
from spack.util.executable import which
|
||||
|
||||
_gnupg_version_re = r"^gpg \(GnuPG\) (.*)$"
|
||||
|
||||
GNUPGHOME = os.getenv('SPACK_GNUPGHOME', spack.paths.gpg_path)
|
||||
|
||||
|
||||
def parse_keys_output(output):
|
||||
_gnupg_version_re = r"^gpg(conf)? \(GnuPG\) (.*)$"
|
||||
_gnupg_home_override = None
|
||||
_global_gpg_instance = None
|
||||
|
||||
|
||||
def get_gnupg_home(gnupg_home=None):
|
||||
"""Returns the directory that should be used as the GNUPGHOME environment
|
||||
variable when calling gpg.
|
||||
|
||||
If a [gnupg_home] is passed directly (and not None), that value will be
|
||||
used.
|
||||
|
||||
Otherwise, if there is an override set (and it is not None), then that
|
||||
value will be used.
|
||||
|
||||
Otherwise, if the environment variable "SPACK_GNUPGHOME" is set, then that
|
||||
value will be used.
|
||||
|
||||
Otherwise, the default gpg path for Spack will be used.
|
||||
|
||||
See also: gnupg_home_override()
|
||||
"""
|
||||
return (gnupg_home or
|
||||
_gnupg_home_override or
|
||||
os.getenv('SPACK_GNUPGHOME') or
|
||||
spack.paths.gpg_path)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def gnupg_home_override(new_gnupg_home):
|
||||
global _gnupg_home_override
|
||||
global _global_gpg_instance
|
||||
|
||||
old_gnupg_home_override = _gnupg_home_override
|
||||
old_global_gpg_instance = _global_gpg_instance
|
||||
|
||||
_gnupg_home_override = new_gnupg_home
|
||||
_global_gpg_instance = None
|
||||
|
||||
yield
|
||||
|
||||
_gnupg_home_override = old_gnupg_home_override
|
||||
_global_gpg_instance = old_global_gpg_instance
|
||||
|
||||
|
||||
def get_global_gpg_instance():
|
||||
global _global_gpg_instance
|
||||
if _global_gpg_instance is None:
|
||||
_global_gpg_instance = Gpg()
|
||||
return _global_gpg_instance
|
||||
|
||||
|
||||
def parse_secret_keys_output(output):
|
||||
keys = []
|
||||
found_sec = False
|
||||
for line in output.split('\n'):
|
||||
|
@ -31,43 +84,230 @@ def parse_keys_output(output):
|
|||
return keys
|
||||
|
||||
|
||||
def parse_public_keys_output(output):
|
||||
keys = []
|
||||
found_pub = False
|
||||
for line in output.split('\n'):
|
||||
if found_pub:
|
||||
if line.startswith('fpr'):
|
||||
keys.append(line.split(':')[9])
|
||||
found_pub = False
|
||||
elif line.startswith('ssb'):
|
||||
found_pub = False
|
||||
elif line.startswith('pub'):
|
||||
found_pub = True
|
||||
return keys
|
||||
|
||||
|
||||
cached_property = getattr(functools, 'cached_property', None)
|
||||
|
||||
# If older python version has no cached_property, emulate it here.
|
||||
# TODO(opadron): maybe this shim should be moved to llnl.util.lang?
|
||||
if not cached_property:
|
||||
def cached_property(*args, **kwargs):
|
||||
result = property(llnl.util.lang.memoized(*args, **kwargs))
|
||||
attr = result.fget.__name__
|
||||
|
||||
@result.deleter
|
||||
def result(self):
|
||||
getattr(type(self), attr).fget.cache.pop((self,), None)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class GpgConstants(object):
|
||||
@cached_property
|
||||
def target_version(self):
|
||||
return spack.version.Version('2')
|
||||
|
||||
@cached_property
|
||||
def gpgconf_string(self):
|
||||
exe_str = spack.util.executable.which_string(
|
||||
'gpgconf', 'gpg2conf', 'gpgconf2')
|
||||
|
||||
no_gpgconf_msg = (
|
||||
'Spack requires gpgconf version >= 2\n'
|
||||
' To install a suitable version using Spack, run\n'
|
||||
' spack install gnupg@2:\n'
|
||||
' and load it by running\n'
|
||||
' spack load gnupg@2:')
|
||||
|
||||
if not exe_str:
|
||||
raise SpackGPGError(no_gpgconf_msg)
|
||||
|
||||
exe = spack.util.executable.Executable(exe_str)
|
||||
output = exe('--version', output=str)
|
||||
match = re.search(_gnupg_version_re, output, re.M)
|
||||
|
||||
if not match:
|
||||
raise SpackGPGError('Could not determine gpgconf version')
|
||||
|
||||
if spack.version.Version(match.group(2)) < self.target_version:
|
||||
raise SpackGPGError(no_gpgconf_msg)
|
||||
|
||||
# ensure that the gpgconf we found can run "gpgconf --create-socketdir"
|
||||
try:
|
||||
exe('--dry-run', '--create-socketdir')
|
||||
except spack.util.executable.ProcessError:
|
||||
# no dice
|
||||
exe_str = None
|
||||
|
||||
return exe_str
|
||||
|
||||
@cached_property
|
||||
def gpg_string(self):
|
||||
exe_str = spack.util.executable.which_string('gpg2', 'gpg')
|
||||
|
||||
no_gpg_msg = (
|
||||
'Spack requires gpg version >= 2\n'
|
||||
' To install a suitable version using Spack, run\n'
|
||||
' spack install gnupg@2:\n'
|
||||
' and load it by running\n'
|
||||
' spack load gnupg@2:')
|
||||
|
||||
if not exe_str:
|
||||
raise SpackGPGError(no_gpg_msg)
|
||||
|
||||
exe = spack.util.executable.Executable(exe_str)
|
||||
output = exe('--version', output=str)
|
||||
match = re.search(_gnupg_version_re, output, re.M)
|
||||
|
||||
if not match:
|
||||
raise SpackGPGError('Could not determine gpg version')
|
||||
|
||||
if spack.version.Version(match.group(2)) < self.target_version:
|
||||
raise SpackGPGError(no_gpg_msg)
|
||||
|
||||
return exe_str
|
||||
|
||||
@cached_property
|
||||
def user_run_dir(self):
|
||||
# Try to ensure that (/var)/run/user/$(id -u) exists so that
|
||||
# `gpgconf --create-socketdir` can be run later.
|
||||
#
|
||||
# NOTE(opadron): This action helps prevent a large class of
|
||||
# "file-name-too-long" errors in gpg.
|
||||
|
||||
try:
|
||||
has_suitable_gpgconf = bool(GpgConstants.gpgconf_string)
|
||||
except SpackGPGError:
|
||||
has_suitable_gpgconf = False
|
||||
|
||||
# If there is no suitable gpgconf, don't even bother trying to
|
||||
# precreate a user run dir.
|
||||
if not has_suitable_gpgconf:
|
||||
return None
|
||||
|
||||
result = None
|
||||
for var_run in ('/run', '/var/run'):
|
||||
if not os.path.exists(var_run):
|
||||
continue
|
||||
|
||||
var_run_user = os.path.join(var_run, 'user')
|
||||
try:
|
||||
if not os.path.exists(var_run_user):
|
||||
os.mkdir(var_run_user)
|
||||
os.chmod(var_run_user, 0o777)
|
||||
|
||||
user_dir = os.path.join(var_run_user, str(os.getuid()))
|
||||
|
||||
if not os.path.exists(user_dir):
|
||||
os.mkdir(user_dir)
|
||||
os.chmod(user_dir, 0o700)
|
||||
|
||||
# If the above operation fails due to lack of permissions, then
|
||||
# just carry on without running gpgconf and hope for the best.
|
||||
#
|
||||
# NOTE(opadron): Without a dir in which to create a socket for IPC,
|
||||
# gnupg may fail if GNUPGHOME is set to a path that
|
||||
# is too long, where "too long" in this context is
|
||||
# actually quite short; somewhere in the
|
||||
# neighborhood of more than 100 characters.
|
||||
#
|
||||
# TODO(opadron): Maybe a warning should be printed in this case?
|
||||
except OSError as exc:
|
||||
if exc.errno not in (errno.EPERM, errno.EACCES):
|
||||
raise
|
||||
user_dir = None
|
||||
|
||||
# return the last iteration that provides a usable user run dir
|
||||
if user_dir is not None:
|
||||
result = user_dir
|
||||
|
||||
return result
|
||||
|
||||
def clear(self):
|
||||
for attr in ('gpgconf_string', 'gpg_string', 'user_run_dir'):
|
||||
try:
|
||||
delattr(self, attr)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
GpgConstants = GpgConstants()
|
||||
|
||||
|
||||
def ensure_gpg(reevaluate=False):
|
||||
if reevaluate:
|
||||
GpgConstants.clear()
|
||||
|
||||
if GpgConstants.user_run_dir is not None:
|
||||
GpgConstants.gpgconf_string
|
||||
|
||||
GpgConstants.gpg_string
|
||||
return True
|
||||
|
||||
|
||||
def has_gpg(*args, **kwargs):
|
||||
try:
|
||||
return ensure_gpg(*args, **kwargs)
|
||||
except SpackGPGError:
|
||||
return False
|
||||
|
||||
|
||||
# NOTE(opadron): When adding methods to this class, consider adding convenience
|
||||
# wrapper functions further down in this file.
|
||||
class Gpg(object):
|
||||
_gpg = None
|
||||
def __init__(self, gnupg_home=None):
|
||||
self.gnupg_home = get_gnupg_home(gnupg_home)
|
||||
|
||||
@staticmethod
|
||||
def gpg():
|
||||
# TODO: Support loading up a GPG environment from a built gpg.
|
||||
if Gpg._gpg is None:
|
||||
gpg = which('gpg2', 'gpg')
|
||||
@cached_property
|
||||
def prep(self):
|
||||
# Make sure that suitable versions of gpgconf and gpg are available
|
||||
ensure_gpg()
|
||||
|
||||
if not gpg:
|
||||
raise SpackGPGError("Spack requires gpg version 2 or higher.")
|
||||
# Make sure that the GNUPGHOME exists
|
||||
if not os.path.exists(self.gnupg_home):
|
||||
os.makedirs(self.gnupg_home)
|
||||
os.chmod(self.gnupg_home, 0o700)
|
||||
|
||||
# ensure that the version is actually >= 2 if we find 'gpg'
|
||||
if gpg.name == 'gpg':
|
||||
output = gpg('--version', output=str)
|
||||
match = re.search(_gnupg_version_re, output, re.M)
|
||||
if not os.path.isdir(self.gnupg_home):
|
||||
raise SpackGPGError(
|
||||
'GNUPGHOME "{0}" exists and is not a directory'.format(
|
||||
self.gnupg_home))
|
||||
|
||||
if not match:
|
||||
raise SpackGPGError("Couldn't determine version of gpg")
|
||||
if GpgConstants.user_run_dir is not None:
|
||||
self.gpgconf_exe('--create-socketdir')
|
||||
|
||||
v = spack.version.Version(match.group(1))
|
||||
if v < spack.version.Version('2'):
|
||||
raise SpackGPGError("Spack requires GPG version >= 2")
|
||||
return True
|
||||
|
||||
# make the GNU PG path if we need to
|
||||
# TODO: does this need to be in the spack directory?
|
||||
# we should probably just use GPG's regular conventions
|
||||
if not os.path.exists(GNUPGHOME):
|
||||
os.makedirs(GNUPGHOME)
|
||||
os.chmod(GNUPGHOME, 0o700)
|
||||
gpg.add_default_env('GNUPGHOME', GNUPGHOME)
|
||||
@cached_property
|
||||
def gpgconf_exe(self):
|
||||
exe = spack.util.executable.Executable(GpgConstants.gpgconf_string)
|
||||
exe.add_default_env('GNUPGHOME', self.gnupg_home)
|
||||
return exe
|
||||
|
||||
Gpg._gpg = gpg
|
||||
return Gpg._gpg
|
||||
@cached_property
|
||||
def gpg_exe(self):
|
||||
exe = spack.util.executable.Executable(GpgConstants.gpg_string)
|
||||
exe.add_default_env('GNUPGHOME', self.gnupg_home)
|
||||
return exe
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
def __call__(self, *args, **kwargs):
|
||||
if self.prep:
|
||||
return self.gpg_exe(*args, **kwargs)
|
||||
|
||||
def create(self, **kwargs):
|
||||
r, w = os.pipe()
|
||||
r = os.fdopen(r, 'r')
|
||||
w = os.fdopen(w, 'w')
|
||||
|
@ -83,64 +323,108 @@ def create(cls, **kwargs):
|
|||
%%commit
|
||||
''' % kwargs)
|
||||
w.close()
|
||||
cls.gpg()('--gen-key', '--batch', input=r)
|
||||
self('--gen-key', '--batch', input=r)
|
||||
r.close()
|
||||
|
||||
@classmethod
|
||||
def signing_keys(cls):
|
||||
output = cls.gpg()('--list-secret-keys', '--with-colons',
|
||||
'--fingerprint', '--fingerprint', output=str)
|
||||
return parse_keys_output(output)
|
||||
def signing_keys(self, *args):
|
||||
output = self('--list-secret-keys', '--with-colons', '--fingerprint',
|
||||
*args, output=str)
|
||||
return parse_secret_keys_output(output)
|
||||
|
||||
@classmethod
|
||||
def export_keys(cls, location, *keys):
|
||||
cls.gpg()('--armor', '--export', '--output', location, *keys)
|
||||
def public_keys(self, *args):
|
||||
output = self('--list-public-keys', '--with-colons', '--fingerprint',
|
||||
*args, output=str)
|
||||
return parse_public_keys_output(output)
|
||||
|
||||
@classmethod
|
||||
def trust(cls, keyfile):
|
||||
cls.gpg()('--import', keyfile)
|
||||
def export_keys(self, location, *keys):
|
||||
self('--batch', '--yes',
|
||||
'--armor', '--export',
|
||||
'--output', location, *keys)
|
||||
|
||||
@classmethod
|
||||
def untrust(cls, signing, *keys):
|
||||
args = [
|
||||
'--yes',
|
||||
'--batch',
|
||||
]
|
||||
def trust(self, keyfile):
|
||||
self('--import', keyfile)
|
||||
|
||||
def untrust(self, signing, *keys):
|
||||
if signing:
|
||||
signing_args = args + ['--delete-secret-keys'] + list(keys)
|
||||
cls.gpg()(*signing_args)
|
||||
args.append('--delete-keys')
|
||||
args.extend(keys)
|
||||
cls.gpg()(*args)
|
||||
skeys = self.signing_keys(*keys)
|
||||
self('--batch', '--yes', '--delete-secret-keys', *skeys)
|
||||
|
||||
@classmethod
|
||||
def sign(cls, key, file, output, clearsign=False):
|
||||
args = [
|
||||
'--armor',
|
||||
'--default-key', key,
|
||||
'--output', output,
|
||||
file,
|
||||
]
|
||||
if clearsign:
|
||||
args.insert(0, '--clearsign')
|
||||
else:
|
||||
args.insert(0, '--detach-sign')
|
||||
cls.gpg()(*args)
|
||||
pkeys = self.public_keys(*keys)
|
||||
self('--batch', '--yes', '--delete-keys', *pkeys)
|
||||
|
||||
@classmethod
|
||||
def verify(cls, signature, file, suppress_warnings=False):
|
||||
if suppress_warnings:
|
||||
cls.gpg()('--verify', signature, file, error=str)
|
||||
else:
|
||||
cls.gpg()('--verify', signature, file)
|
||||
def sign(self, key, file, output, clearsign=False):
|
||||
self(('--clearsign' if clearsign else '--detach-sign'),
|
||||
'--armor', '--default-key', key,
|
||||
'--output', output, file)
|
||||
|
||||
@classmethod
|
||||
def list(cls, trusted, signing):
|
||||
def verify(self, signature, file, suppress_warnings=False):
|
||||
self('--verify', signature, file,
|
||||
**({'error': str} if suppress_warnings else {}))
|
||||
|
||||
def list(self, trusted, signing):
|
||||
if trusted:
|
||||
cls.gpg()('--list-public-keys')
|
||||
self('--list-public-keys')
|
||||
|
||||
if signing:
|
||||
cls.gpg()('--list-secret-keys')
|
||||
self('--list-secret-keys')
|
||||
|
||||
|
||||
class SpackGPGError(spack.error.SpackError):
|
||||
"""Class raised when GPG errors are detected."""
|
||||
|
||||
|
||||
# Convenience wrappers for methods of the Gpg class
|
||||
|
||||
# __call__ is a bit of a special case, since the Gpg instance is, itself, the
|
||||
# "thing" that is being called.
|
||||
@functools.wraps(Gpg.__call__)
|
||||
def gpg(*args, **kwargs):
|
||||
return get_global_gpg_instance()(*args, **kwargs)
|
||||
|
||||
|
||||
gpg.name = 'gpg'
|
||||
|
||||
|
||||
@functools.wraps(Gpg.create)
|
||||
def create(*args, **kwargs):
|
||||
return get_global_gpg_instance().create(*args, **kwargs)
|
||||
|
||||
|
||||
@functools.wraps(Gpg.signing_keys)
|
||||
def signing_keys(*args, **kwargs):
|
||||
return get_global_gpg_instance().signing_keys(*args, **kwargs)
|
||||
|
||||
|
||||
@functools.wraps(Gpg.public_keys)
|
||||
def public_keys(*args, **kwargs):
|
||||
return get_global_gpg_instance().public_keys(*args, **kwargs)
|
||||
|
||||
|
||||
@functools.wraps(Gpg.export_keys)
|
||||
def export_keys(*args, **kwargs):
|
||||
return get_global_gpg_instance().export_keys(*args, **kwargs)
|
||||
|
||||
|
||||
@functools.wraps(Gpg.trust)
|
||||
def trust(*args, **kwargs):
|
||||
return get_global_gpg_instance().trust(*args, **kwargs)
|
||||
|
||||
|
||||
@functools.wraps(Gpg.untrust)
|
||||
def untrust(*args, **kwargs):
|
||||
return get_global_gpg_instance().untrust(*args, **kwargs)
|
||||
|
||||
|
||||
@functools.wraps(Gpg.sign)
|
||||
def sign(*args, **kwargs):
|
||||
return get_global_gpg_instance().sign(*args, **kwargs)
|
||||
|
||||
|
||||
@functools.wraps(Gpg.verify)
|
||||
def verify(*args, **kwargs):
|
||||
return get_global_gpg_instance().verify(*args, **kwargs)
|
||||
|
||||
|
||||
@functools.wraps(Gpg.list)
|
||||
def list(*args, **kwargs):
|
||||
return get_global_gpg_instance().list(*args, **kwargs)
|
||||
|
|
|
@ -224,7 +224,7 @@ def url_exists(url):
|
|||
try:
|
||||
read_from_url(url)
|
||||
return True
|
||||
except URLError:
|
||||
except (SpackWebError, URLError):
|
||||
return False
|
||||
|
||||
|
||||
|
@ -295,15 +295,27 @@ def _iter_s3_prefix(client, url, num_entries=1024):
|
|||
break
|
||||
|
||||
|
||||
def list_url(url):
|
||||
def _iter_local_prefix(path):
|
||||
for root, _, files in os.walk(path):
|
||||
for f in files:
|
||||
yield os.path.relpath(os.path.join(root, f), path)
|
||||
|
||||
|
||||
def list_url(url, recursive=False):
|
||||
url = url_util.parse(url)
|
||||
|
||||
local_path = url_util.local_file_path(url)
|
||||
if local_path:
|
||||
return os.listdir(local_path)
|
||||
if recursive:
|
||||
return list(_iter_local_prefix(local_path))
|
||||
return [subpath for subpath in os.listdir(local_path)
|
||||
if os.path.isfile(os.path.join(local_path, subpath))]
|
||||
|
||||
if url.scheme == 's3':
|
||||
s3 = s3_util.create_s3_session(url)
|
||||
if recursive:
|
||||
return list(_iter_s3_prefix(s3, url))
|
||||
|
||||
return list(set(
|
||||
key.split('/', 1)[0]
|
||||
for key in _iter_s3_prefix(s3, url)))
|
||||
|
|
|
@ -906,7 +906,7 @@ _spack_gpg() {
|
|||
then
|
||||
SPACK_COMPREPLY="-h --help"
|
||||
else
|
||||
SPACK_COMPREPLY="verify trust untrust sign create list init export"
|
||||
SPACK_COMPREPLY="verify trust untrust sign create list init export publish"
|
||||
fi
|
||||
}
|
||||
|
||||
|
@ -972,6 +972,15 @@ _spack_gpg_export() {
|
|||
fi
|
||||
}
|
||||
|
||||
_spack_gpg_publish() {
|
||||
if $list_options
|
||||
then
|
||||
SPACK_COMPREPLY="-h --help -d --directory -m --mirror-name --mirror-url --rebuild-index"
|
||||
else
|
||||
_keys
|
||||
fi
|
||||
}
|
||||
|
||||
_spack_graph() {
|
||||
if $list_options
|
||||
then
|
||||
|
|
Loading…
Reference in a new issue