mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-21 22:07:51 +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,
|
||||
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
|
||||
)
|
||||
|
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