mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-22 06:17:50 +00:00
grid-manager stand-alone, via Click
This commit is contained in:
parent
c7f4a1a157
commit
2118a2446e
7
setup.py
7
setup.py
@ -410,6 +410,11 @@ setup(name="tahoe-lafs", # also set in __init__.py
|
|||||||
},
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
setup_requires=setup_requires,
|
setup_requires=setup_requires,
|
||||||
entry_points = { 'console_scripts': [ 'tahoe = allmydata.scripts.runner:run' ] },
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'tahoe = allmydata.scripts.runner:run',
|
||||||
|
'grid-manager = allmydata.cli.grid_manager:grid_manager',
|
||||||
|
]
|
||||||
|
},
|
||||||
**setup_args
|
**setup_args
|
||||||
)
|
)
|
||||||
|
0
src/allmydata/cli/__init__.py
Normal file
0
src/allmydata/cli/__init__.py
Normal file
207
src/allmydata/cli/grid_manager.py
Normal file
207
src/allmydata/cli/grid_manager.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
from datetime import (
|
||||||
|
datetime,
|
||||||
|
)
|
||||||
|
import json
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from twisted.python.filepath import (
|
||||||
|
FilePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
from allmydata.crypto import (
|
||||||
|
ed25519,
|
||||||
|
)
|
||||||
|
from allmydata.util.abbreviate import (
|
||||||
|
abbreviate_time,
|
||||||
|
)
|
||||||
|
from allmydata.grid_manager import (
|
||||||
|
create_grid_manager,
|
||||||
|
save_grid_manager,
|
||||||
|
load_grid_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option(
|
||||||
|
'--config', '-c',
|
||||||
|
type=click.Path(),
|
||||||
|
help="Configuration directory (or - for stdin)",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def grid_manager(ctx, config):
|
||||||
|
"""
|
||||||
|
A Tahoe Grid Manager issues certificates to storage-servers
|
||||||
|
|
||||||
|
A Tahoe client with one or more Grid Manager public keys
|
||||||
|
configured will only upload to a Storage Server that presents a
|
||||||
|
valid certificate signed by one of the configured Grid
|
||||||
|
Manager keys.
|
||||||
|
|
||||||
|
Grid Manager configuration can be in a local directory or given
|
||||||
|
via stdin. It contains long-term secret information (a private
|
||||||
|
signing key) and should be kept safe.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
"""
|
||||||
|
Availble to all sub-commands as Click's context.obj
|
||||||
|
"""
|
||||||
|
_grid_manager = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def grid_manager(self):
|
||||||
|
if self._grid_manager is None:
|
||||||
|
config_path = _config_path_from_option(config)
|
||||||
|
self._grid_manager = load_grid_manager(config_path, config)
|
||||||
|
return self._grid_manager
|
||||||
|
|
||||||
|
ctx.obj = Config()
|
||||||
|
|
||||||
|
|
||||||
|
@grid_manager.command()
|
||||||
|
@click.pass_context
|
||||||
|
def create(ctx):
|
||||||
|
"""
|
||||||
|
Make a new Grid Manager
|
||||||
|
"""
|
||||||
|
config_location = ctx.parent.params["config"]
|
||||||
|
fp = None
|
||||||
|
if config_location != '-':
|
||||||
|
fp = FilePath(config_location)
|
||||||
|
if fp.exists():
|
||||||
|
raise click.ClickException(
|
||||||
|
"The directory '{}' already exists.".format(config_location)
|
||||||
|
)
|
||||||
|
|
||||||
|
gm = create_grid_manager()
|
||||||
|
save_grid_manager(fp, gm)
|
||||||
|
|
||||||
|
|
||||||
|
@grid_manager.command()
|
||||||
|
@click.pass_obj
|
||||||
|
def public_identity(config):
|
||||||
|
"""
|
||||||
|
Show the public identity key of a Grid Manager
|
||||||
|
|
||||||
|
This is what you give to clients to add to their configuration so
|
||||||
|
they use announcements from this Grid Manager
|
||||||
|
"""
|
||||||
|
click.echo(config.grid_manager.public_identity())
|
||||||
|
|
||||||
|
|
||||||
|
@grid_manager.command()
|
||||||
|
@click.argument("name")
|
||||||
|
@click.argument("public_key", type=click.UNPROCESSED)
|
||||||
|
@click.pass_context
|
||||||
|
def add(ctx, name, public_key):
|
||||||
|
"""
|
||||||
|
Add a new storage-server by name to a Grid Manager
|
||||||
|
|
||||||
|
PUBLIC_KEY is the contents of a node.pubkey file from a Tahoe
|
||||||
|
node-directory. NAME is an arbitrary label.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ctx.obj.grid_manager.add_storage_server(
|
||||||
|
name,
|
||||||
|
ed25519.verifying_key_from_string(public_key),
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
raise click.ClickException(
|
||||||
|
"A storage-server called '{}' already exists".format(name)
|
||||||
|
)
|
||||||
|
save_grid_manager(
|
||||||
|
_config_path_from_option(ctx.parent.params["config"]),
|
||||||
|
ctx.obj.grid_manager,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@grid_manager.command()
|
||||||
|
@click.argument("name")
|
||||||
|
@click.pass_context
|
||||||
|
def remove(ctx, name):
|
||||||
|
"""
|
||||||
|
Remove an existing storage-server by name from a Grid Manager
|
||||||
|
"""
|
||||||
|
fp = _config_path_from_option(ctx.parent.params["config"])
|
||||||
|
try:
|
||||||
|
ctx.obj.grid_manager.remove_storage_server(name)
|
||||||
|
except KeyError:
|
||||||
|
raise click.ClickException(
|
||||||
|
"No storage-server called '{}' exists".format(name)
|
||||||
|
)
|
||||||
|
cert_count = 0
|
||||||
|
if fp is not None:
|
||||||
|
while fp.child('{}.cert.{}'.format(name, cert_count)).exists():
|
||||||
|
fp.child('{}.cert.{}'.format(name, cert_count)).remove()
|
||||||
|
cert_count += 1
|
||||||
|
|
||||||
|
save_grid_manager(fp, ctx.obj.grid_manager)
|
||||||
|
|
||||||
|
|
||||||
|
@grid_manager.command()
|
||||||
|
@click.pass_context
|
||||||
|
def list(ctx):
|
||||||
|
"""
|
||||||
|
List all storage-servers known to a Grid Manager
|
||||||
|
"""
|
||||||
|
fp = _config_path_from_option(ctx.parent.params["config"])
|
||||||
|
for name in sorted(ctx.obj.grid_manager.storage_servers.keys()):
|
||||||
|
blank_name = " " * len(name)
|
||||||
|
click.echo("{}: {}".format(name, ctx.obj.grid_manager.storage_servers[name].public_key()))
|
||||||
|
if fp:
|
||||||
|
cert_count = 0
|
||||||
|
while fp.child('{}.cert.{}'.format(name, cert_count)).exists():
|
||||||
|
container = json.load(fp.child('{}.cert.{}'.format(name, cert_count)).open('r'))
|
||||||
|
cert_data = json.loads(container['certificate'])
|
||||||
|
expires = datetime.utcfromtimestamp(cert_data['expires'])
|
||||||
|
delta = datetime.utcnow() - expires
|
||||||
|
click.echo("{} cert {}: ".format(blank_name, cert_count), nl=False)
|
||||||
|
if delta.total_seconds() < 0:
|
||||||
|
click.echo("valid until {} ({})".format(expires, abbreviate_time(delta)))
|
||||||
|
else:
|
||||||
|
click.echo("expired {} ({})".format(expires, abbreviate_time(delta)))
|
||||||
|
cert_count += 1
|
||||||
|
|
||||||
|
|
||||||
|
@grid_manager.command()
|
||||||
|
@click.argument("name")
|
||||||
|
@click.argument(
|
||||||
|
"expiry_days",
|
||||||
|
type=click.IntRange(1, 5*365), # XXX is 5 years a good maximum?
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def sign(ctx, name, expiry_days):
|
||||||
|
"""
|
||||||
|
sign a new certificate
|
||||||
|
"""
|
||||||
|
fp = _config_path_from_option(ctx.parent.params["config"])
|
||||||
|
expiry_seconds = int(expiry_days) * 86400
|
||||||
|
|
||||||
|
try:
|
||||||
|
certificate = ctx.obj.grid_manager.sign(name, expiry_seconds)
|
||||||
|
except KeyError:
|
||||||
|
raise click.ClickException(
|
||||||
|
"No storage-server called '{}' exists".format(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
certificate_data = json.dumps(certificate, indent=4)
|
||||||
|
click.echo(certificate_data)
|
||||||
|
if fp is not None:
|
||||||
|
next_serial = 0
|
||||||
|
while fp.child("{}.cert.{}".format(name, next_serial)).exists():
|
||||||
|
next_serial += 1
|
||||||
|
with fp.child('{}.cert.{}'.format(name, next_serial)).open('w') as f:
|
||||||
|
f.write(certificate_data)
|
||||||
|
|
||||||
|
|
||||||
|
def _config_path_from_option(config):
|
||||||
|
"""
|
||||||
|
:param string config: a path or -
|
||||||
|
:returns: a FilePath instance or None
|
||||||
|
"""
|
||||||
|
if config == "-":
|
||||||
|
return None
|
||||||
|
return FilePath(config)
|
217
src/allmydata/grid_manager.py
Normal file
217
src/allmydata/grid_manager.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from datetime import (
|
||||||
|
datetime,
|
||||||
|
timedelta,
|
||||||
|
)
|
||||||
|
|
||||||
|
from allmydata.crypto import (
|
||||||
|
ed25519,
|
||||||
|
)
|
||||||
|
from allmydata.util import (
|
||||||
|
fileutil,
|
||||||
|
base32,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _GridManagerStorageServer(object):
|
||||||
|
"""
|
||||||
|
A Grid Manager's notion of a storage server
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, public_key, certificates):
|
||||||
|
self.name = name
|
||||||
|
self._public_key = public_key
|
||||||
|
self._certificates = [] if certificates is None else certificates
|
||||||
|
|
||||||
|
def add_certificate(self, certificate):
|
||||||
|
self._certificates.append(certificate)
|
||||||
|
|
||||||
|
def public_key(self):
|
||||||
|
return ed25519.string_from_verifying_key(self._public_key)
|
||||||
|
|
||||||
|
def marshal(self):
|
||||||
|
return {
|
||||||
|
u"public_key": self.public_key(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_grid_manager():
|
||||||
|
"""
|
||||||
|
Create a new Grid Manager with a fresh keypair
|
||||||
|
"""
|
||||||
|
private_key, public_key = ed25519.create_signing_keypair()
|
||||||
|
return _GridManager(
|
||||||
|
ed25519.string_from_signing_key(private_key),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_grid_manager(config_path, config_location):
|
||||||
|
"""
|
||||||
|
Load a Grid Manager from existing configuration.
|
||||||
|
|
||||||
|
:param FilePath config_path: the configuratino location (or None for
|
||||||
|
stdin)
|
||||||
|
|
||||||
|
:param str config_location: a string describing the config's location
|
||||||
|
|
||||||
|
:returns: a GridManager instance
|
||||||
|
"""
|
||||||
|
if config_path is None:
|
||||||
|
config_file = sys.stdin
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
config_file = config_path.child("config.json").open("r")
|
||||||
|
except IOError:
|
||||||
|
raise ValueError(
|
||||||
|
"'{}' is not a Grid Manager config-directory".format(config)
|
||||||
|
)
|
||||||
|
with config_file:
|
||||||
|
config = json.load(config_file)
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid Grid Manager config in '{}'".format(config_location)
|
||||||
|
)
|
||||||
|
if 'private_key' not in config:
|
||||||
|
raise ValueError(
|
||||||
|
"Grid Manager config from '{}' requires a 'private_key'".format(
|
||||||
|
config_location,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private_key_bytes = config['private_key'].encode('ascii')
|
||||||
|
try:
|
||||||
|
private_key, public_key = ed25519.signing_keypair_from_string(private_key_bytes)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid Grid Manager private_key: {}".format(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
storage_servers = dict()
|
||||||
|
for name, srv_config in config.get(u'storage_servers', {}).items():
|
||||||
|
if not 'public_key' in srv_config:
|
||||||
|
raise ValueError(
|
||||||
|
"No 'public_key' for storage server '{}'".format(name)
|
||||||
|
)
|
||||||
|
storage_servers[name] = _GridManagerStorageServer(
|
||||||
|
name,
|
||||||
|
ed25519.verifying_key_from_string(srv_config['public_key'].encode('ascii')),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
gm_version = config.get(u'grid_manager_config_version', None)
|
||||||
|
if gm_version != 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Missing or unknown version '{}' of Grid Manager config".format(
|
||||||
|
gm_version
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return _GridManager(private_key_bytes, storage_servers)
|
||||||
|
|
||||||
|
|
||||||
|
class _GridManager(object):
|
||||||
|
"""
|
||||||
|
A Grid Manager's configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, private_key_bytes, storage_servers):
|
||||||
|
self._storage_servers = dict() if storage_servers is None else storage_servers
|
||||||
|
self._private_key_bytes = private_key_bytes
|
||||||
|
self._private_key, self._public_key = ed25519.signing_keypair_from_string(self._private_key_bytes)
|
||||||
|
self._version = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def storage_servers(self):
|
||||||
|
return self._storage_servers
|
||||||
|
|
||||||
|
def public_identity(self):
|
||||||
|
return ed25519.string_from_verifying_key(self._public_key)
|
||||||
|
|
||||||
|
def sign(self, name, expiry_seconds):
|
||||||
|
try:
|
||||||
|
srv = self._storage_servers[name]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError(
|
||||||
|
u"No storage server named '{}'".format(name)
|
||||||
|
)
|
||||||
|
expiration = datetime.utcnow() + timedelta(seconds=expiry_seconds)
|
||||||
|
epoch_offset = (expiration - datetime(1970, 1, 1)).total_seconds()
|
||||||
|
cert_info = {
|
||||||
|
"expires": epoch_offset,
|
||||||
|
"public_key": srv.public_key(),
|
||||||
|
"version": 1,
|
||||||
|
}
|
||||||
|
cert_data = json.dumps(cert_info, separators=(',',':'), sort_keys=True).encode('utf8')
|
||||||
|
sig = ed25519.sign_data(self._private_key, cert_data)
|
||||||
|
certificate = {
|
||||||
|
u"certificate": cert_data,
|
||||||
|
u"signature": base32.b2a(sig),
|
||||||
|
}
|
||||||
|
|
||||||
|
vk = ed25519.verifying_key_from_signing_key(self._private_key)
|
||||||
|
ed25519.verify_signature(vk, sig, cert_data)
|
||||||
|
|
||||||
|
return certificate
|
||||||
|
|
||||||
|
def add_storage_server(self, name, public_key):
|
||||||
|
"""
|
||||||
|
:param name: a user-meaningful name for the server
|
||||||
|
:param public_key: ed25519.VerifyingKey the public-key of the
|
||||||
|
storage provider (e.g. from the contents of node.pubkey
|
||||||
|
for the client)
|
||||||
|
"""
|
||||||
|
if name in self._storage_servers:
|
||||||
|
raise KeyError(
|
||||||
|
"Already have a storage server called '{}'".format(name)
|
||||||
|
)
|
||||||
|
ss = _GridManagerStorageServer(name, public_key, None)
|
||||||
|
self._storage_servers[name] = ss
|
||||||
|
return ss
|
||||||
|
|
||||||
|
def remove_storage_server(self, name):
|
||||||
|
"""
|
||||||
|
:param name: a user-meaningful name for the server
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
del self._storage_servers[name]
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError(
|
||||||
|
"No storage server called '{}'".format(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
def marshal(self):
|
||||||
|
data = {
|
||||||
|
u"grid_manager_config_version": self._version,
|
||||||
|
u"private_key": self._private_key_bytes.decode('ascii'),
|
||||||
|
}
|
||||||
|
if self._storage_servers:
|
||||||
|
data[u"storage_servers"] = {
|
||||||
|
name: srv.marshal()
|
||||||
|
for name, srv
|
||||||
|
in self._storage_servers.items()
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def save_grid_manager(file_path, grid_manager):
|
||||||
|
"""
|
||||||
|
Writes a Grid Manager configuration.
|
||||||
|
|
||||||
|
:param file_path: a FilePath specifying where to write the config
|
||||||
|
(if None, stdout is used)
|
||||||
|
|
||||||
|
:param grid_manager: a _GridManager instance
|
||||||
|
"""
|
||||||
|
data = json.dumps(
|
||||||
|
grid_manager.marshal(),
|
||||||
|
indent=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_path is None:
|
||||||
|
print("{}\n".format(data))
|
||||||
|
else:
|
||||||
|
fileutil.make_dirs(file_path.path, mode=0o700)
|
||||||
|
with file_path.child("config.json").open("w") as f:
|
||||||
|
f.write("{}\n".format(data))
|
@ -1,497 +0,0 @@
|
|||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from allmydata.scripts.common import BaseOptions
|
|
||||||
from allmydata.util.abbreviate import abbreviate_time
|
|
||||||
from twisted.python import usage
|
|
||||||
from twisted.python.filepath import FilePath
|
|
||||||
from allmydata.util import fileutil
|
|
||||||
from allmydata.util import base32
|
|
||||||
from allmydata.crypto import ed25519
|
|
||||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
|
||||||
|
|
||||||
|
|
||||||
class CreateOptions(BaseOptions):
|
|
||||||
description = (
|
|
||||||
"Create a new identity key and configuration of a Grid Manager"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ShowIdentityOptions(BaseOptions):
|
|
||||||
description = (
|
|
||||||
"Show the public identity key of a Grid Manager\n"
|
|
||||||
"\n"
|
|
||||||
"This is what you give to clients to add to their configuration"
|
|
||||||
" so they use announcements from this Grid Manager"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AddOptions(BaseOptions):
|
|
||||||
description = (
|
|
||||||
"Add a new storage-server's key to a Grid Manager configuration\n"
|
|
||||||
"using NAME and PUBIC_KEY (comes from a node.pubkey file)"
|
|
||||||
)
|
|
||||||
|
|
||||||
def getSynopsis(self):
|
|
||||||
return "{} add NAME PUBLIC_KEY".format(super(AddOptions, self).getSynopsis())
|
|
||||||
|
|
||||||
def parseArgs(self, *args, **kw):
|
|
||||||
BaseOptions.parseArgs(self, **kw)
|
|
||||||
if len(args) != 2:
|
|
||||||
raise usage.UsageError(
|
|
||||||
"Requires two arguments: name public_key"
|
|
||||||
)
|
|
||||||
self['name'] = unicode(args[0])
|
|
||||||
try:
|
|
||||||
# WTF?! why does it want 'str' and not six.text_type?
|
|
||||||
self['storage_public_key'] = ed25519.verifying_key_from_string(args[1])
|
|
||||||
except Exception as e:
|
|
||||||
raise usage.UsageError(
|
|
||||||
"Invalid public_key argument: {}".format(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveOptions(BaseOptions):
|
|
||||||
description = (
|
|
||||||
"Remove a storage-server from a Grid Manager configuration"
|
|
||||||
)
|
|
||||||
|
|
||||||
def parseArgs(self, *args, **kw):
|
|
||||||
BaseOptions.parseArgs(self, **kw)
|
|
||||||
if len(args) != 1:
|
|
||||||
raise usage.UsageError(
|
|
||||||
"Requires one arguments: name"
|
|
||||||
)
|
|
||||||
self['name'] = unicode(args[0])
|
|
||||||
|
|
||||||
|
|
||||||
class ListOptions(BaseOptions):
|
|
||||||
description = (
|
|
||||||
"List all storage servers in this Grid Manager"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SignOptions(BaseOptions):
|
|
||||||
description = (
|
|
||||||
"Create and sign a new certificate for a storage-server"
|
|
||||||
)
|
|
||||||
|
|
||||||
def getSynopsis(self):
|
|
||||||
return "{} NAME EXPIRY_DAYS".format(super(SignOptions, self).getSynopsis())
|
|
||||||
|
|
||||||
def parseArgs(self, *args, **kw):
|
|
||||||
BaseOptions.parseArgs(self, **kw)
|
|
||||||
if len(args) != 2:
|
|
||||||
raise usage.UsageError(
|
|
||||||
"Requires two arguments: name expiry_days"
|
|
||||||
)
|
|
||||||
self['name'] = unicode(args[0])
|
|
||||||
self['expiry_days'] = int(args[1])
|
|
||||||
if self['expiry_days'] < 1 or self['expiry_days'] > 20*365:
|
|
||||||
raise usage.UsageError(
|
|
||||||
"Certificate expires in an unreasonable number of days: {}".format(
|
|
||||||
self['expiry_days'],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GridManagerOptions(BaseOptions):
|
|
||||||
subCommands = [
|
|
||||||
["create", None, CreateOptions, "Create a Grid Manager."],
|
|
||||||
["public-identity", None, ShowIdentityOptions, "Get the public-key for this Grid Manager."],
|
|
||||||
["add", None, AddOptions, "Add a storage server to this Grid Manager."],
|
|
||||||
["remove", None, RemoveOptions, "Remove a storage server from this Grid Manager."],
|
|
||||||
["list", None, ListOptions, "List all storage servers in this Grid Manager."],
|
|
||||||
["sign", None, SignOptions, "Create and sign a new Storage Certificate."],
|
|
||||||
]
|
|
||||||
|
|
||||||
optParameters = [
|
|
||||||
("config", "c", None, "How to find the Grid Manager's configuration")
|
|
||||||
]
|
|
||||||
|
|
||||||
def postOptions(self):
|
|
||||||
if not hasattr(self, 'subOptions'):
|
|
||||||
raise usage.UsageError("must specify a subcommand")
|
|
||||||
if self['config'] is None:
|
|
||||||
raise usage.UsageError("Must supply configuration with --config")
|
|
||||||
|
|
||||||
description = (
|
|
||||||
'A "grid-manager" consists of some data defining a keypair (along with '
|
|
||||||
'some other details) and Tahoe sub-commands to manipulate the data and '
|
|
||||||
'produce certificates to give to storage-servers. Certificates assert '
|
|
||||||
'the statement: "Grid Manager X suggests you use storage-server Y to '
|
|
||||||
'upload shares to" (X and Y are public-keys).'
|
|
||||||
'\n\n'
|
|
||||||
'Clients can use Grid Managers to decide which storage servers to '
|
|
||||||
'upload shares to. They do this by adding one or more Grid Manager '
|
|
||||||
'public keys to their config.'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_gridmanager():
|
|
||||||
"""
|
|
||||||
:return: an object providing the GridManager interface initialized
|
|
||||||
with a new random keypair
|
|
||||||
"""
|
|
||||||
private_key, public_key = ed25519.create_signing_keypair()
|
|
||||||
return _GridManager(
|
|
||||||
ed25519.string_from_signing_key(private_key),
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create(gridoptions, options):
|
|
||||||
"""
|
|
||||||
Create a new Grid Manager
|
|
||||||
"""
|
|
||||||
gm_config = gridoptions['config']
|
|
||||||
|
|
||||||
# pre-conditions check
|
|
||||||
fp = None
|
|
||||||
if gm_config.strip() != '-':
|
|
||||||
fp = FilePath(gm_config.strip())
|
|
||||||
if fp.exists():
|
|
||||||
raise usage.UsageError(
|
|
||||||
"The directory '{}' already exists.".format(gm_config)
|
|
||||||
)
|
|
||||||
|
|
||||||
gm = _create_gridmanager()
|
|
||||||
_save_gridmanager_config(fp, gm)
|
|
||||||
|
|
||||||
|
|
||||||
class _GridManagerStorageServer(object):
|
|
||||||
"""
|
|
||||||
A Grid Manager's notion of a storage server
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, public_key, certificates):
|
|
||||||
self.name = name
|
|
||||||
self._public_key = public_key
|
|
||||||
self._certificates = [] if certificates is None else certificates
|
|
||||||
|
|
||||||
def add_certificate(self, certificate):
|
|
||||||
self._certificates.append(certificate)
|
|
||||||
|
|
||||||
def public_key(self):
|
|
||||||
return ed25519.string_from_verifying_key(self._public_key)
|
|
||||||
|
|
||||||
def marshal(self):
|
|
||||||
return {
|
|
||||||
u"public_key": self.public_key(),
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GridManager(object):
|
|
||||||
"""
|
|
||||||
A Grid Manager's configuration.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_config(config, config_location):
|
|
||||||
if not config:
|
|
||||||
raise ValueError(
|
|
||||||
"Invalid Grid Manager config in '{}'".format(config_location)
|
|
||||||
)
|
|
||||||
if 'private_key' not in config:
|
|
||||||
raise ValueError(
|
|
||||||
"Grid Manager config from '{}' requires a 'private_key'".format(
|
|
||||||
config_location,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private_key_bytes = config['private_key'].encode('ascii')
|
|
||||||
try:
|
|
||||||
private_key, public_key = ed25519.signing_keypair_from_string(private_key_bytes)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(
|
|
||||||
"Invalid Grid Manager private_key: {}".format(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
storage_servers = dict()
|
|
||||||
for name, srv_config in config.get(u'storage_servers', {}).items():
|
|
||||||
if not 'public_key' in srv_config:
|
|
||||||
raise ValueError(
|
|
||||||
"No 'public_key' for storage server '{}'".format(name)
|
|
||||||
)
|
|
||||||
storage_servers[name] = _GridManagerStorageServer(
|
|
||||||
name,
|
|
||||||
ed25519.verifying_key_from_string(srv_config['public_key'].encode('ascii')),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
gm_version = config.get(u'grid_manager_config_version', None)
|
|
||||||
if gm_version != 0:
|
|
||||||
raise ValueError(
|
|
||||||
"Missing or unknown version '{}' of Grid Manager config".format(
|
|
||||||
gm_version
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return _GridManager(private_key_bytes, storage_servers)
|
|
||||||
|
|
||||||
def __init__(self, private_key_bytes, storage_servers):
|
|
||||||
self._storage_servers = dict() if storage_servers is None else storage_servers
|
|
||||||
self._private_key_bytes = private_key_bytes
|
|
||||||
self._private_key, self._public_key = ed25519.signing_keypair_from_string(self._private_key_bytes)
|
|
||||||
self._version = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def storage_servers(self):
|
|
||||||
return self._storage_servers
|
|
||||||
|
|
||||||
def public_identity(self):
|
|
||||||
return ed25519.string_from_verifying_key(self._public_key)
|
|
||||||
|
|
||||||
def sign(self, name, expiry_seconds):
|
|
||||||
try:
|
|
||||||
srv = self._storage_servers[name]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError(
|
|
||||||
u"No storage server named '{}'".format(name)
|
|
||||||
)
|
|
||||||
expiration = datetime.utcnow() + timedelta(seconds=expiry_seconds)
|
|
||||||
epoch_offset = (expiration - datetime(1970, 1, 1)).total_seconds()
|
|
||||||
cert_info = {
|
|
||||||
"expires": epoch_offset,
|
|
||||||
"public_key": srv.public_key(),
|
|
||||||
"version": 1,
|
|
||||||
}
|
|
||||||
cert_data = json.dumps(cert_info, separators=(',',':'), sort_keys=True).encode('utf8')
|
|
||||||
sig = ed25519.sign_data(self._private_key, cert_data)
|
|
||||||
certificate = {
|
|
||||||
u"certificate": cert_data,
|
|
||||||
u"signature": base32.b2a(sig),
|
|
||||||
}
|
|
||||||
|
|
||||||
vk = ed25519.verifying_key_from_signing_key(self._private_key)
|
|
||||||
ed25519.verify_signature(vk, sig, cert_data)
|
|
||||||
|
|
||||||
return certificate
|
|
||||||
|
|
||||||
def add_storage_server(self, name, public_key):
|
|
||||||
"""
|
|
||||||
:param name: a user-meaningful name for the server
|
|
||||||
:param public_key: ed25519.VerifyingKey the public-key of the
|
|
||||||
storage provider (e.g. from the contents of node.pubkey
|
|
||||||
for the client)
|
|
||||||
"""
|
|
||||||
if name in self._storage_servers:
|
|
||||||
raise KeyError(
|
|
||||||
"Already have a storage server called '{}'".format(name)
|
|
||||||
)
|
|
||||||
ss = _GridManagerStorageServer(name, public_key, None)
|
|
||||||
self._storage_servers[name] = ss
|
|
||||||
return ss
|
|
||||||
|
|
||||||
def remove_storage_server(self, name):
|
|
||||||
"""
|
|
||||||
:param name: a user-meaningful name for the server
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
del self._storage_servers[name]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError(
|
|
||||||
"No storage server called '{}'".format(name)
|
|
||||||
)
|
|
||||||
|
|
||||||
def marshal(self):
|
|
||||||
data = {
|
|
||||||
u"grid_manager_config_version": self._version,
|
|
||||||
u"private_key": self._private_key_bytes.decode('ascii'),
|
|
||||||
}
|
|
||||||
if self._storage_servers:
|
|
||||||
data[u"storage_servers"] = {
|
|
||||||
name: srv.marshal()
|
|
||||||
for name, srv
|
|
||||||
in self._storage_servers.items()
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def _save_gridmanager_config(file_path, grid_manager):
|
|
||||||
"""
|
|
||||||
Writes a Grid Manager configuration.
|
|
||||||
|
|
||||||
:param file_path: a FilePath specifying where to write the config
|
|
||||||
(if None, stdout is used)
|
|
||||||
|
|
||||||
:param grid_manager: a _GridManager instance
|
|
||||||
"""
|
|
||||||
data = json.dumps(
|
|
||||||
grid_manager.marshal(),
|
|
||||||
indent=4,
|
|
||||||
)
|
|
||||||
|
|
||||||
if file_path is None:
|
|
||||||
print("{}\n".format(data))
|
|
||||||
else:
|
|
||||||
fileutil.make_dirs(file_path.path, mode=0o700)
|
|
||||||
with file_path.child("config.json").open("w") as f:
|
|
||||||
f.write("{}\n".format(data))
|
|
||||||
|
|
||||||
|
|
||||||
def _load_gridmanager_config(fp):
|
|
||||||
"""
|
|
||||||
Loads a Grid Manager configuration and returns it (a dict) after
|
|
||||||
validating. Exceptions if the config can't be found, or has
|
|
||||||
problems.
|
|
||||||
|
|
||||||
:param FilePath fp: None for stdin or a path to a Grid Manager
|
|
||||||
configuration directory
|
|
||||||
"""
|
|
||||||
if fp is None:
|
|
||||||
gm = json.load(sys.stdin)
|
|
||||||
else:
|
|
||||||
with fp.child("config.json").open("r") as f:
|
|
||||||
gm = json.load(f)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return _GridManager.from_config(gm, fp or "<stdin>")
|
|
||||||
except ValueError as e:
|
|
||||||
raise usage.UsageError(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
def _show_identity(gridoptions, options):
|
|
||||||
"""
|
|
||||||
Output the public-key of a Grid Manager
|
|
||||||
"""
|
|
||||||
gm_config = gridoptions['config'].strip()
|
|
||||||
assert gm_config is not None
|
|
||||||
|
|
||||||
gm = _load_gridmanager_config(gm_config)
|
|
||||||
print(gm.public_identity())
|
|
||||||
|
|
||||||
|
|
||||||
def _add(gridoptions, options):
|
|
||||||
"""
|
|
||||||
Add a new storage-server by name to a Grid Manager
|
|
||||||
"""
|
|
||||||
gm_config = gridoptions['config'].strip()
|
|
||||||
fp = FilePath(gm_config) if gm_config.strip() != '-' else None
|
|
||||||
|
|
||||||
gm = _load_gridmanager_config(gm_config)
|
|
||||||
try:
|
|
||||||
gm.add_storage_server(
|
|
||||||
options['name'],
|
|
||||||
options['storage_public_key'],
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
raise usage.UsageError(
|
|
||||||
"A storage-server called '{}' already exists".format(options['name'])
|
|
||||||
)
|
|
||||||
|
|
||||||
_save_gridmanager_config(fp, gm)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _remove(gridoptions, options):
|
|
||||||
"""
|
|
||||||
Remove an existing storage-server by name from a Grid Manager
|
|
||||||
"""
|
|
||||||
gm_config = gridoptions['config'].strip()
|
|
||||||
fp = FilePath(gm_config) if gm_config.strip() != '-' else None
|
|
||||||
gm = _load_gridmanager_config(gm_config)
|
|
||||||
|
|
||||||
try:
|
|
||||||
gm.remove_storage_server(options['name'])
|
|
||||||
except KeyError:
|
|
||||||
raise usage.UsageError(
|
|
||||||
"No storage-server called '{}' exists".format(options['name'])
|
|
||||||
)
|
|
||||||
cert_count = 0
|
|
||||||
if fp is not None:
|
|
||||||
while fp.child('{}.cert.{}'.format(options['name'], cert_count)).exists():
|
|
||||||
fp.child('{}.cert.{}'.format(options['name'], cert_count)).remove()
|
|
||||||
cert_count += 1
|
|
||||||
|
|
||||||
_save_gridmanager_config(fp, gm)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _list(gridoptions, options):
|
|
||||||
"""
|
|
||||||
List all storage-servers known to a Grid Manager
|
|
||||||
"""
|
|
||||||
gm_config = gridoptions['config'].strip()
|
|
||||||
fp = FilePath(gm_config) if gm_config.strip() != '-' else None
|
|
||||||
|
|
||||||
gm = _load_gridmanager_config(gm_config)
|
|
||||||
for name in sorted(gm.storage_servers.keys()):
|
|
||||||
print("{}: {}".format(name, gm.storage_servers[name].public_key()))
|
|
||||||
if fp:
|
|
||||||
cert_count = 0
|
|
||||||
while fp.child('{}.cert.{}'.format(name, cert_count)).exists():
|
|
||||||
container = json.load(fp.child('{}.cert.{}'.format(name, cert_count)).open('r'))
|
|
||||||
cert_data = json.loads(container['certificate'])
|
|
||||||
expires = datetime.utcfromtimestamp(cert_data['expires'])
|
|
||||||
delta = datetime.utcnow() - expires
|
|
||||||
if delta.total_seconds() < 0:
|
|
||||||
print("{}: cert {}: valid until {} ({})".format(name, cert_count, expires, abbreviate_time(delta)))
|
|
||||||
else:
|
|
||||||
print("{}: cert {}: expired ({})".format(name, cert_count, abbreviate_time(delta)))
|
|
||||||
cert_count += 1
|
|
||||||
|
|
||||||
|
|
||||||
def _sign(gridoptions, options):
|
|
||||||
"""
|
|
||||||
sign a new certificate
|
|
||||||
"""
|
|
||||||
gm_config = gridoptions['config'].strip()
|
|
||||||
fp = FilePath(gm_config) if gm_config.strip() != '-' else None
|
|
||||||
gm = _load_gridmanager_config(gm_config)
|
|
||||||
|
|
||||||
expiry_seconds = int(options['expiry_days']) * 86400
|
|
||||||
|
|
||||||
try:
|
|
||||||
certificate = gm.sign(options['name'], expiry_seconds)
|
|
||||||
except KeyError:
|
|
||||||
raise usage.UsageError(
|
|
||||||
"No storage-server called '{}' exists".format(options['name'])
|
|
||||||
)
|
|
||||||
|
|
||||||
certificate_data = json.dumps(certificate, indent=4)
|
|
||||||
print(certificate_data)
|
|
||||||
if fp is not None:
|
|
||||||
next_serial = 0
|
|
||||||
while fp.child("{}.cert.{}".format(options['name'], next_serial)).exists():
|
|
||||||
next_serial += 1
|
|
||||||
with fp.child('{}.cert.{}'.format(options['name'], next_serial)).open('w') as f:
|
|
||||||
f.write(certificate_data)
|
|
||||||
|
|
||||||
|
|
||||||
grid_manager_commands = {
|
|
||||||
CreateOptions: _create,
|
|
||||||
ShowIdentityOptions: _show_identity,
|
|
||||||
AddOptions: _add,
|
|
||||||
RemoveOptions: _remove,
|
|
||||||
ListOptions: _list,
|
|
||||||
SignOptions: _sign,
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlineCallbacks
|
|
||||||
def gridmanager(config):
|
|
||||||
"""
|
|
||||||
Runs the 'tahoe grid-manager' command.
|
|
||||||
"""
|
|
||||||
if config.subCommand is None:
|
|
||||||
print(config)
|
|
||||||
returnValue(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
f = grid_manager_commands[config.subOptions.__class__]
|
|
||||||
except KeyError:
|
|
||||||
print(config.subOptions, grid_manager_commands.keys())
|
|
||||||
print("Unknown command 'tahoe grid-manager {}': no such grid-manager subcommand".format(config.subCommand))
|
|
||||||
returnValue(2)
|
|
||||||
|
|
||||||
x = yield f(config, config.subOptions)
|
|
||||||
returnValue(x)
|
|
||||||
|
|
||||||
subCommands = [
|
|
||||||
["grid-manager", None, GridManagerOptions,
|
|
||||||
"Grid Manager subcommands: use 'tahoe grid-manager' for a list."],
|
|
||||||
]
|
|
||||||
|
|
||||||
dispatch = {
|
|
||||||
"grid-manager": gridmanager,
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user