1 | """ |
---|
2 | Ported to Python 3. |
---|
3 | """ |
---|
4 | |
---|
5 | from six import ensure_binary |
---|
6 | |
---|
7 | from twisted.python import usage |
---|
8 | from twisted.python.filepath import ( |
---|
9 | FilePath, |
---|
10 | ) |
---|
11 | from allmydata.scripts.common import ( |
---|
12 | BaseOptions, |
---|
13 | BasedirOptions, |
---|
14 | ) |
---|
15 | from allmydata.storage import ( |
---|
16 | crawler, |
---|
17 | expirer, |
---|
18 | ) |
---|
19 | from allmydata.scripts.types_ import SubCommands |
---|
20 | from allmydata.client import read_config |
---|
21 | from allmydata.grid_manager import ( |
---|
22 | parse_grid_manager_certificate, |
---|
23 | ) |
---|
24 | from allmydata.scripts.cli import _default_nodedir |
---|
25 | from allmydata.util.encodingutil import argv_to_abspath |
---|
26 | from allmydata.util import jsonbytes |
---|
27 | |
---|
28 | class GenerateKeypairOptions(BaseOptions): |
---|
29 | |
---|
30 | def getUsage(self, width=None): |
---|
31 | t = BaseOptions.getUsage(self, width) |
---|
32 | t += """ |
---|
33 | Generate a public/private keypair, dumped to stdout as two lines of ASCII.. |
---|
34 | |
---|
35 | """ |
---|
36 | return t |
---|
37 | |
---|
38 | def print_keypair(options): |
---|
39 | from allmydata.crypto import ed25519 |
---|
40 | out = options.stdout |
---|
41 | private_key, public_key = ed25519.create_signing_keypair() |
---|
42 | print("private:", str(ed25519.string_from_signing_key(private_key), "ascii"), |
---|
43 | file=out) |
---|
44 | print("public:", str(ed25519.string_from_verifying_key(public_key), "ascii"), |
---|
45 | file=out) |
---|
46 | |
---|
47 | class DerivePubkeyOptions(BaseOptions): |
---|
48 | def parseArgs(self, privkey): |
---|
49 | self.privkey = privkey |
---|
50 | |
---|
51 | def getSynopsis(self): |
---|
52 | return "Usage: tahoe [global-options] admin derive-pubkey PRIVKEY" |
---|
53 | |
---|
54 | def getUsage(self, width=None): |
---|
55 | t = BaseOptions.getUsage(self, width) |
---|
56 | t += """ |
---|
57 | Given a private (signing) key that was previously generated with |
---|
58 | generate-keypair, derive the public key and print it to stdout. |
---|
59 | |
---|
60 | """ |
---|
61 | return t |
---|
62 | |
---|
63 | def derive_pubkey(options): |
---|
64 | out = options.stdout |
---|
65 | from allmydata.crypto import ed25519 |
---|
66 | privkey_vs = options.privkey |
---|
67 | privkey_vs = ensure_binary(privkey_vs) |
---|
68 | private_key, public_key = ed25519.signing_keypair_from_string(privkey_vs) |
---|
69 | print("private:", str(ed25519.string_from_signing_key(private_key), "ascii"), file=out) |
---|
70 | print("public:", str(ed25519.string_from_verifying_key(public_key), "ascii"), file=out) |
---|
71 | return 0 |
---|
72 | |
---|
73 | |
---|
74 | class MigrateCrawlerOptions(BasedirOptions): |
---|
75 | |
---|
76 | def getSynopsis(self): |
---|
77 | return "Usage: tahoe [global-options] admin migrate-crawler" |
---|
78 | |
---|
79 | def getUsage(self, width=None): |
---|
80 | t = BasedirOptions.getUsage(self, width) |
---|
81 | t += ( |
---|
82 | "The crawler data is now stored as JSON to avoid" |
---|
83 | " potential security issues with pickle files.\n\nIf" |
---|
84 | " you are confident the state files in the 'storage/'" |
---|
85 | " subdirectory of your node are trustworthy, run this" |
---|
86 | " command to upgrade them to JSON.\n\nThe files are:" |
---|
87 | " lease_checker.history, lease_checker.state, and" |
---|
88 | " bucket_counter.state" |
---|
89 | ) |
---|
90 | return t |
---|
91 | |
---|
92 | |
---|
93 | class AddGridManagerCertOptions(BaseOptions): |
---|
94 | """ |
---|
95 | Options for add-grid-manager-cert |
---|
96 | """ |
---|
97 | |
---|
98 | optParameters = [ |
---|
99 | ['filename', 'f', None, "Filename of the certificate ('-', a dash, for stdin)"], |
---|
100 | ['name', 'n', None, "Name to give this certificate"], |
---|
101 | ] |
---|
102 | |
---|
103 | def getSynopsis(self): |
---|
104 | return "Usage: tahoe [global-options] admin add-grid-manager-cert [options]" |
---|
105 | |
---|
106 | def postOptions(self) -> None: |
---|
107 | assert self.parent is not None |
---|
108 | assert self.parent.parent is not None |
---|
109 | |
---|
110 | if self['name'] is None: |
---|
111 | raise usage.UsageError( |
---|
112 | "Must provide --name option" |
---|
113 | ) |
---|
114 | if self['filename'] is None: |
---|
115 | raise usage.UsageError( |
---|
116 | "Must provide --filename option" |
---|
117 | ) |
---|
118 | |
---|
119 | data: str |
---|
120 | if self['filename'] == '-': |
---|
121 | print("reading certificate from stdin", file=self.parent.parent.stderr) # type: ignore[attr-defined] |
---|
122 | data = self.parent.parent.stdin.read() # type: ignore[attr-defined] |
---|
123 | if len(data) == 0: |
---|
124 | raise usage.UsageError( |
---|
125 | "Reading certificate from stdin failed" |
---|
126 | ) |
---|
127 | else: |
---|
128 | with open(self['filename'], 'r') as f: |
---|
129 | data = f.read() |
---|
130 | |
---|
131 | try: |
---|
132 | self.certificate_data = parse_grid_manager_certificate(data) |
---|
133 | except ValueError as e: |
---|
134 | raise usage.UsageError( |
---|
135 | "Error parsing certificate: {}".format(e) |
---|
136 | ) |
---|
137 | |
---|
138 | def getUsage(self, width=None): |
---|
139 | t = BaseOptions.getUsage(self, width) |
---|
140 | t += ( |
---|
141 | "Adds a Grid Manager certificate to a Storage Server.\n\n" |
---|
142 | "The certificate will be copied into the base-dir and config\n" |
---|
143 | "will be added to 'tahoe.cfg', which will be re-written. A\n" |
---|
144 | "restart is required for changes to take effect.\n\n" |
---|
145 | "The human who operates a Grid Manager would produce such a\n" |
---|
146 | "certificate and communicate it securely to you.\n" |
---|
147 | ) |
---|
148 | return t |
---|
149 | |
---|
150 | |
---|
151 | def migrate_crawler(options): |
---|
152 | out = options.stdout |
---|
153 | storage = FilePath(options['basedir']).child("storage") |
---|
154 | |
---|
155 | conversions = [ |
---|
156 | (storage.child("lease_checker.state"), crawler._convert_pickle_state_to_json), |
---|
157 | (storage.child("bucket_counter.state"), crawler._convert_pickle_state_to_json), |
---|
158 | (storage.child("lease_checker.history"), expirer._convert_pickle_state_to_json), |
---|
159 | ] |
---|
160 | |
---|
161 | for fp, converter in conversions: |
---|
162 | existed = fp.exists() |
---|
163 | newfp = crawler._upgrade_pickle_to_json(fp, converter) |
---|
164 | if existed: |
---|
165 | print("Converted '{}' to '{}'".format(fp.path, newfp.path), file=out) |
---|
166 | else: |
---|
167 | if newfp.exists(): |
---|
168 | print("Already converted: '{}'".format(newfp.path), file=out) |
---|
169 | else: |
---|
170 | print("Not found: '{}'".format(fp.path), file=out) |
---|
171 | |
---|
172 | |
---|
173 | def add_grid_manager_cert(options): |
---|
174 | """ |
---|
175 | Add a new Grid Manager certificate to our config |
---|
176 | """ |
---|
177 | # XXX is there really not already a function for this? |
---|
178 | if options.parent.parent['node-directory']: |
---|
179 | nd = argv_to_abspath(options.parent.parent['node-directory']) |
---|
180 | else: |
---|
181 | nd = _default_nodedir |
---|
182 | |
---|
183 | config = read_config(nd, "portnum") |
---|
184 | cert_fname = "{}.cert".format(options['name']) |
---|
185 | cert_path = FilePath(config.get_config_path(cert_fname)) |
---|
186 | cert_bytes = jsonbytes.dumps_bytes(options.certificate_data, indent=4) + b'\n' |
---|
187 | cert_name = options['name'] |
---|
188 | |
---|
189 | if cert_path.exists(): |
---|
190 | msg = "Already have certificate for '{}' (at {})".format( |
---|
191 | options['name'], |
---|
192 | cert_path.path, |
---|
193 | ) |
---|
194 | print(msg, file=options.stderr) |
---|
195 | return 1 |
---|
196 | |
---|
197 | config.set_config("storage", "grid_management", "True") |
---|
198 | config.set_config("grid_manager_certificates", cert_name, cert_fname) |
---|
199 | |
---|
200 | # write all the data out |
---|
201 | with cert_path.open("wb") as f: |
---|
202 | f.write(cert_bytes) |
---|
203 | |
---|
204 | cert_count = len(config.enumerate_section("grid_manager_certificates")) |
---|
205 | print("There are now {} certificates".format(cert_count), |
---|
206 | file=options.stderr) |
---|
207 | |
---|
208 | return 0 |
---|
209 | |
---|
210 | |
---|
211 | class AdminCommand(BaseOptions): |
---|
212 | subCommands = [ |
---|
213 | ("generate-keypair", None, GenerateKeypairOptions, |
---|
214 | "Generate a public/private keypair, write to stdout."), |
---|
215 | ("derive-pubkey", None, DerivePubkeyOptions, |
---|
216 | "Derive a public key from a private key."), |
---|
217 | ("migrate-crawler", None, MigrateCrawlerOptions, |
---|
218 | "Write the crawler-history data as JSON."), |
---|
219 | ("add-grid-manager-cert", None, AddGridManagerCertOptions, |
---|
220 | "Add a Grid Manager-provided certificate to a storage " |
---|
221 | "server's config."), |
---|
222 | ] |
---|
223 | def postOptions(self): |
---|
224 | if not hasattr(self, 'subOptions'): |
---|
225 | raise usage.UsageError("must specify a subcommand") |
---|
226 | def getSynopsis(self): |
---|
227 | return "Usage: tahoe [global-options] admin SUBCOMMAND" |
---|
228 | def getUsage(self, width=None): |
---|
229 | t = BaseOptions.getUsage(self, width) |
---|
230 | t += """ |
---|
231 | Please run e.g. 'tahoe admin generate-keypair --help' for more details on |
---|
232 | each subcommand. |
---|
233 | """ |
---|
234 | return t |
---|
235 | |
---|
236 | |
---|
237 | subDispatch = { |
---|
238 | "generate-keypair": print_keypair, |
---|
239 | "derive-pubkey": derive_pubkey, |
---|
240 | "migrate-crawler": migrate_crawler, |
---|
241 | "add-grid-manager-cert": add_grid_manager_cert, |
---|
242 | } |
---|
243 | |
---|
244 | |
---|
245 | def do_admin(options): |
---|
246 | so = options.subOptions |
---|
247 | so.stdout = options.stdout |
---|
248 | so.stderr = options.stderr |
---|
249 | f = subDispatch[options.subCommand] |
---|
250 | return f(so) |
---|
251 | |
---|
252 | |
---|
253 | subCommands : SubCommands = [ |
---|
254 | ("admin", None, AdminCommand, "admin subcommands: use 'tahoe admin' for a list"), |
---|
255 | ] |
---|
256 | |
---|
257 | dispatch = { |
---|
258 | "admin": do_admin, |
---|
259 | } |
---|