source: trunk/src/allmydata/cli/grid_manager.py

Last change on this file was a9128d8, checked in by meejah <meejah@…>, at 2023-07-29T10:41:03Z

Merge branch 'master' into 2916.grid-manager-integration-tests.2

  • Property mode set to 100644
File size: 6.2 KB
Line 
1"""
2A CLI for configuring a grid manager.
3"""
4
5from typing import Optional
6from datetime import (
7    timedelta,
8)
9
10import click
11
12from twisted.python.filepath import (
13    FilePath,
14)
15
16from allmydata.crypto import (
17    ed25519,
18)
19from allmydata.util.abbreviate import (
20    abbreviate_time,
21)
22from allmydata.grid_manager import (
23    create_grid_manager,
24    save_grid_manager,
25    load_grid_manager,
26    current_datetime_with_zone,
27)
28from allmydata.util import jsonbytes as json
29
30
31@click.group()
32@click.option(
33    '--config', '-c',
34    type=click.Path(),
35    help="Configuration directory (or - for stdin)",
36    required=True,
37)
38@click.pass_context
39def grid_manager(ctx, config):
40    """
41    A Tahoe Grid Manager issues certificates to storage-servers
42
43    A Tahoe client with one or more Grid Manager public keys
44    configured will only upload to a Storage Server that presents a
45    valid certificate signed by one of the configured Grid
46    Manager keys.
47
48    Grid Manager configuration can be in a local directory or given
49    via stdin. It contains long-term secret information (a private
50    signing key) and should be kept safe.
51    """
52
53    class Config(object):
54        """
55        Available to all sub-commands as Click's context.obj
56        """
57        _grid_manager = None
58
59        @property
60        def grid_manager(self):
61            if self._grid_manager is None:
62                config_path = _config_path_from_option(config)
63                try:
64                    self._grid_manager = load_grid_manager(config_path)
65                except ValueError as e:
66                    raise click.ClickException(
67                        "Error loading Grid Manager from '{}': {}".format(config, e)
68                    )
69            return self._grid_manager
70
71    ctx.obj = Config()
72
73
74@grid_manager.command()
75@click.pass_context
76def create(ctx):
77    """
78    Make a new Grid Manager
79    """
80    config_location = ctx.parent.params["config"]
81    fp = None
82    if config_location != '-':
83        fp = FilePath(config_location)
84
85    gm = create_grid_manager()
86    try:
87        save_grid_manager(fp, gm)
88    except OSError as e:
89        raise click.ClickException(
90            "Can't create '{}': {}".format(config_location, e)
91        )
92
93
94@grid_manager.command()
95@click.pass_obj
96def public_identity(config):
97    """
98    Show the public identity key of a Grid Manager
99
100    This is what you give to clients to add to their configuration so
101    they use announcements from this Grid Manager
102    """
103    click.echo(config.grid_manager.public_identity())
104
105
106@grid_manager.command()
107@click.argument("name")
108@click.argument("public_key", type=click.STRING)
109@click.pass_context
110def add(ctx, name, public_key):
111    """
112    Add a new storage-server by name to a Grid Manager
113
114    PUBLIC_KEY is the contents of a node.pubkey file from a Tahoe
115    node-directory. NAME is an arbitrary label.
116    """
117    public_key = public_key.encode("ascii")
118    try:
119        ctx.obj.grid_manager.add_storage_server(
120            name,
121            ed25519.verifying_key_from_string(public_key),
122        )
123    except KeyError:
124        raise click.ClickException(
125            "A storage-server called '{}' already exists".format(name)
126        )
127    save_grid_manager(
128        _config_path_from_option(ctx.parent.params["config"]),
129        ctx.obj.grid_manager,
130        create=False,
131    )
132    return 0
133
134
135@grid_manager.command()
136@click.argument("name")
137@click.pass_context
138def remove(ctx, name):
139    """
140    Remove an existing storage-server by name from a Grid Manager
141    """
142    fp = _config_path_from_option(ctx.parent.params["config"])
143    try:
144        ctx.obj.grid_manager.remove_storage_server(name)
145    except KeyError:
146        raise click.ClickException(
147            "No storage-server called '{}' exists".format(name)
148        )
149    cert_count = 0
150    if fp is not None:
151        while fp.child('{}.cert.{}'.format(name, cert_count)).exists():
152            fp.child('{}.cert.{}'.format(name, cert_count)).remove()
153            cert_count += 1
154
155    save_grid_manager(fp, ctx.obj.grid_manager, create=False)
156
157
158@grid_manager.command()  # noqa: F811
159@click.pass_context
160def list(ctx):
161    """
162    List all storage-servers known to a Grid Manager
163    """
164    for name in sorted(ctx.obj.grid_manager.storage_servers.keys()):
165        blank_name = " " * len(name)
166        click.echo("{}: {}".format(
167            name,
168            str(ctx.obj.grid_manager.storage_servers[name].public_key_string(), "utf-8")))
169        for cert in ctx.obj.grid_manager.storage_servers[name].certificates:
170            delta = current_datetime_with_zone() - cert.expires
171            click.echo("{}  cert {}: ".format(blank_name, cert.index), nl=False)
172            if delta.total_seconds() < 0:
173                click.echo("valid until {} ({})".format(cert.expires, abbreviate_time(delta)))
174            else:
175                click.echo("expired {} ({})".format(cert.expires, abbreviate_time(delta)))
176
177
178@grid_manager.command()
179@click.argument("name")
180@click.argument(
181    "expiry_days",
182    type=click.IntRange(1, 5*365),  # XXX is 5 years a good maximum?
183)
184@click.pass_context
185def sign(ctx, name, expiry_days):
186    """
187    sign a new certificate
188    """
189    fp = _config_path_from_option(ctx.parent.params["config"])
190    expiry = timedelta(days=expiry_days)
191
192    try:
193        certificate = ctx.obj.grid_manager.sign(name, expiry)
194    except KeyError:
195        raise click.ClickException(
196            "No storage-server called '{}' exists".format(name)
197        )
198
199    certificate_data = json.dumps(certificate.marshal(), indent=4)
200    click.echo(certificate_data)
201    if fp is not None:
202        next_serial = 0
203        f = None
204        while f is None:
205            fname = "{}.cert.{}".format(name, next_serial)
206            try:
207                f = fp.child(fname).create()
208            except FileExistsError:
209                f = None
210            except OSError as e:
211                raise click.ClickException(f"{fname}: {e}")
212            next_serial += 1
213        with f:
214            f.write(certificate_data.encode("ascii"))
215
216
217def _config_path_from_option(config: str) -> Optional[FilePath]:
218    """
219    :param str config: a path or -
220    :returns: a FilePath instance or None
221    """
222    if config == "-":
223        return None
224    return FilePath(config)
225
226
227if __name__ == '__main__':
228    grid_manager()  # type: ignore
Note: See TracBrowser for help on using the repository browser.