| 1 | """ |
|---|
| 2 | Tests for the grid manager CLI. |
|---|
| 3 | """ |
|---|
| 4 | |
|---|
| 5 | import os |
|---|
| 6 | from io import ( |
|---|
| 7 | BytesIO, |
|---|
| 8 | ) |
|---|
| 9 | from unittest import ( |
|---|
| 10 | skipIf, |
|---|
| 11 | ) |
|---|
| 12 | |
|---|
| 13 | from twisted.trial.unittest import ( |
|---|
| 14 | TestCase, |
|---|
| 15 | ) |
|---|
| 16 | from allmydata.cli.grid_manager import ( |
|---|
| 17 | grid_manager, |
|---|
| 18 | ) |
|---|
| 19 | |
|---|
| 20 | import click.testing |
|---|
| 21 | |
|---|
| 22 | # these imports support the tests for `tahoe *` subcommands |
|---|
| 23 | from ..common_util import ( |
|---|
| 24 | run_cli, |
|---|
| 25 | ) |
|---|
| 26 | from ..common import ( |
|---|
| 27 | superuser, |
|---|
| 28 | ) |
|---|
| 29 | from twisted.internet.defer import ( |
|---|
| 30 | inlineCallbacks, |
|---|
| 31 | ) |
|---|
| 32 | from twisted.python.filepath import ( |
|---|
| 33 | FilePath, |
|---|
| 34 | ) |
|---|
| 35 | from twisted.python.runtime import ( |
|---|
| 36 | platform, |
|---|
| 37 | ) |
|---|
| 38 | from allmydata.util import jsonbytes as json |
|---|
| 39 | |
|---|
| 40 | class GridManagerCommandLine(TestCase): |
|---|
| 41 | """ |
|---|
| 42 | Test the mechanics of the `grid-manager` command |
|---|
| 43 | """ |
|---|
| 44 | |
|---|
| 45 | def setUp(self): |
|---|
| 46 | self.runner = click.testing.CliRunner() |
|---|
| 47 | super(GridManagerCommandLine, self).setUp() |
|---|
| 48 | |
|---|
| 49 | def invoke_and_check(self, *args, **kwargs): |
|---|
| 50 | """Invoke a command with the runner and ensure it succeeded.""" |
|---|
| 51 | result = self.runner.invoke(*args, **kwargs) |
|---|
| 52 | if result.exception is not None: |
|---|
| 53 | raise result.exc_info[1].with_traceback(result.exc_info[2]) |
|---|
| 54 | self.assertEqual(result.exit_code, 0, result) |
|---|
| 55 | return result |
|---|
| 56 | |
|---|
| 57 | def test_create(self): |
|---|
| 58 | """ |
|---|
| 59 | Create a new grid-manager |
|---|
| 60 | """ |
|---|
| 61 | with self.runner.isolated_filesystem(): |
|---|
| 62 | result = self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 63 | self.assertEqual(["foo"], os.listdir(".")) |
|---|
| 64 | self.assertEqual(["config.json"], os.listdir("./foo")) |
|---|
| 65 | result = self.invoke_and_check(grid_manager, ["--config", "foo", "public-identity"]) |
|---|
| 66 | self.assertTrue(result.output.startswith("pub-v0-")) |
|---|
| 67 | |
|---|
| 68 | def test_load_invalid(self): |
|---|
| 69 | """ |
|---|
| 70 | An invalid config is reported to the user |
|---|
| 71 | """ |
|---|
| 72 | with self.runner.isolated_filesystem(): |
|---|
| 73 | with open("config.json", "wb") as f: |
|---|
| 74 | f.write(json.dumps_bytes({"not": "valid"})) |
|---|
| 75 | result = self.runner.invoke(grid_manager, ["--config", ".", "public-identity"]) |
|---|
| 76 | self.assertNotEqual(result.exit_code, 0) |
|---|
| 77 | self.assertIn( |
|---|
| 78 | "Error loading Grid Manager", |
|---|
| 79 | result.output, |
|---|
| 80 | ) |
|---|
| 81 | |
|---|
| 82 | def test_create_already(self): |
|---|
| 83 | """ |
|---|
| 84 | It's an error to create a new grid-manager in an existing |
|---|
| 85 | directory. |
|---|
| 86 | """ |
|---|
| 87 | with self.runner.isolated_filesystem(): |
|---|
| 88 | result = self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 89 | result = self.runner.invoke(grid_manager, ["--config", "foo", "create"]) |
|---|
| 90 | self.assertEqual(1, result.exit_code) |
|---|
| 91 | self.assertIn( |
|---|
| 92 | "Can't create", |
|---|
| 93 | result.output, |
|---|
| 94 | ) |
|---|
| 95 | |
|---|
| 96 | def test_create_stdout(self): |
|---|
| 97 | """ |
|---|
| 98 | Create a new grid-manager with no files |
|---|
| 99 | """ |
|---|
| 100 | with self.runner.isolated_filesystem(): |
|---|
| 101 | result = self.invoke_and_check(grid_manager, ["--config", "-", "create"]) |
|---|
| 102 | self.assertEqual([], os.listdir(".")) |
|---|
| 103 | config = json.loads(result.output) |
|---|
| 104 | self.assertEqual( |
|---|
| 105 | {"private_key", "grid_manager_config_version"}, |
|---|
| 106 | set(config.keys()), |
|---|
| 107 | ) |
|---|
| 108 | |
|---|
| 109 | def test_list_stdout(self): |
|---|
| 110 | """ |
|---|
| 111 | Load Grid Manager without files (using 'list' subcommand, but any will do) |
|---|
| 112 | """ |
|---|
| 113 | config = { |
|---|
| 114 | "storage_servers": { |
|---|
| 115 | "storage0": { |
|---|
| 116 | "public_key": "pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga" |
|---|
| 117 | } |
|---|
| 118 | }, |
|---|
| 119 | "private_key": "priv-v0-6uinzyaxy3zvscwgsps5pxcfezhrkfb43kvnrbrhhfzyduyqnniq", |
|---|
| 120 | "grid_manager_config_version": 0 |
|---|
| 121 | } |
|---|
| 122 | result = self.invoke_and_check( |
|---|
| 123 | grid_manager, ["--config", "-", "list"], |
|---|
| 124 | input=BytesIO(json.dumps_bytes(config)), |
|---|
| 125 | ) |
|---|
| 126 | self.assertEqual(result.exit_code, 0) |
|---|
| 127 | self.assertEqual( |
|---|
| 128 | "storage0: pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga\n", |
|---|
| 129 | result.output, |
|---|
| 130 | ) |
|---|
| 131 | |
|---|
| 132 | def test_add_and_sign(self): |
|---|
| 133 | """ |
|---|
| 134 | Add a new storage-server and sign a certificate for it |
|---|
| 135 | """ |
|---|
| 136 | pubkey = "pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga" |
|---|
| 137 | with self.runner.isolated_filesystem(): |
|---|
| 138 | self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 139 | self.invoke_and_check(grid_manager, ["--config", "foo", "add", "storage0", pubkey]) |
|---|
| 140 | result = self.invoke_and_check(grid_manager, ["--config", "foo", "sign", "storage0", "10"]) |
|---|
| 141 | sigcert = json.loads(result.output) |
|---|
| 142 | self.assertEqual({"certificate", "signature"}, set(sigcert.keys())) |
|---|
| 143 | cert = json.loads(sigcert['certificate']) |
|---|
| 144 | self.assertEqual(cert["public_key"], pubkey) |
|---|
| 145 | |
|---|
| 146 | def test_add_and_sign_second_cert(self): |
|---|
| 147 | """ |
|---|
| 148 | Add a new storage-server and sign two certificates. |
|---|
| 149 | """ |
|---|
| 150 | pubkey = "pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga" |
|---|
| 151 | with self.runner.isolated_filesystem(): |
|---|
| 152 | self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 153 | self.invoke_and_check(grid_manager, ["--config", "foo", "add", "storage0", pubkey]) |
|---|
| 154 | self.invoke_and_check(grid_manager, ["--config", "foo", "sign", "storage0", "10"]) |
|---|
| 155 | self.invoke_and_check(grid_manager, ["--config", "foo", "sign", "storage0", "10"]) |
|---|
| 156 | # we should now have two certificates stored |
|---|
| 157 | self.assertEqual( |
|---|
| 158 | set(FilePath("foo").listdir()), |
|---|
| 159 | {'storage0.cert.1', 'storage0.cert.0', 'config.json'}, |
|---|
| 160 | ) |
|---|
| 161 | |
|---|
| 162 | def test_add_twice(self): |
|---|
| 163 | """ |
|---|
| 164 | An error is reported trying to add an existing server |
|---|
| 165 | """ |
|---|
| 166 | pubkey0 = "pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga" |
|---|
| 167 | pubkey1 = "pub-v0-5ysc55trfvfvg466v46j4zmfyltgus3y2gdejifctv7h4zkuyveq" |
|---|
| 168 | with self.runner.isolated_filesystem(): |
|---|
| 169 | self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 170 | self.invoke_and_check(grid_manager, ["--config", "foo", "add", "storage0", pubkey0]) |
|---|
| 171 | result = self.runner.invoke(grid_manager, ["--config", "foo", "add", "storage0", pubkey1]) |
|---|
| 172 | self.assertNotEqual(result.exit_code, 0) |
|---|
| 173 | self.assertIn( |
|---|
| 174 | "A storage-server called 'storage0' already exists", |
|---|
| 175 | result.output, |
|---|
| 176 | ) |
|---|
| 177 | |
|---|
| 178 | def test_add_list_remove(self): |
|---|
| 179 | """ |
|---|
| 180 | Add a storage server, list it, remove it. |
|---|
| 181 | """ |
|---|
| 182 | pubkey = "pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga" |
|---|
| 183 | with self.runner.isolated_filesystem(): |
|---|
| 184 | self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 185 | self.invoke_and_check(grid_manager, ["--config", "foo", "add", "storage0", pubkey]) |
|---|
| 186 | self.invoke_and_check(grid_manager, ["--config", "foo", "sign", "storage0", "1"]) |
|---|
| 187 | |
|---|
| 188 | result = self.invoke_and_check(grid_manager, ["--config", "foo", "list"]) |
|---|
| 189 | names = [ |
|---|
| 190 | line.split(':')[0] |
|---|
| 191 | for line in result.output.strip().split('\n') |
|---|
| 192 | if not line.startswith(" ") # "cert" lines start with whitespace |
|---|
| 193 | ] |
|---|
| 194 | self.assertEqual(names, ["storage0"]) |
|---|
| 195 | |
|---|
| 196 | self.invoke_and_check(grid_manager, ["--config", "foo", "remove", "storage0"]) |
|---|
| 197 | |
|---|
| 198 | result = self.invoke_and_check(grid_manager, ["--config", "foo", "list"]) |
|---|
| 199 | self.assertEqual(result.output.strip(), "") |
|---|
| 200 | |
|---|
| 201 | def test_remove_missing(self): |
|---|
| 202 | """ |
|---|
| 203 | Error reported when removing non-existent server |
|---|
| 204 | """ |
|---|
| 205 | with self.runner.isolated_filesystem(): |
|---|
| 206 | self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 207 | result = self.runner.invoke(grid_manager, ["--config", "foo", "remove", "storage0"]) |
|---|
| 208 | self.assertNotEqual(result.exit_code, 0) |
|---|
| 209 | self.assertIn( |
|---|
| 210 | "No storage-server called 'storage0' exists", |
|---|
| 211 | result.output, |
|---|
| 212 | ) |
|---|
| 213 | |
|---|
| 214 | def test_sign_missing(self): |
|---|
| 215 | """ |
|---|
| 216 | Error reported when signing non-existent server |
|---|
| 217 | """ |
|---|
| 218 | with self.runner.isolated_filesystem(): |
|---|
| 219 | self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 220 | result = self.runner.invoke(grid_manager, ["--config", "foo", "sign", "storage0", "42"]) |
|---|
| 221 | self.assertNotEqual(result.exit_code, 0) |
|---|
| 222 | self.assertIn( |
|---|
| 223 | "No storage-server called 'storage0' exists", |
|---|
| 224 | result.output, |
|---|
| 225 | ) |
|---|
| 226 | |
|---|
| 227 | @skipIf(platform.isWindows(), "We don't know how to set permissions on Windows.") |
|---|
| 228 | @skipIf(superuser, "cannot test as superuser with all permissions") |
|---|
| 229 | def test_sign_bad_perms(self): |
|---|
| 230 | """ |
|---|
| 231 | Error reported if we can't create certificate file |
|---|
| 232 | """ |
|---|
| 233 | pubkey = "pub-v0-cbq6hcf3pxcz6ouoafrbktmkixkeuywpcpbcomzd3lqbkq4nmfga" |
|---|
| 234 | with self.runner.isolated_filesystem(): |
|---|
| 235 | self.invoke_and_check(grid_manager, ["--config", "foo", "create"]) |
|---|
| 236 | self.invoke_and_check(grid_manager, ["--config", "foo", "add", "storage0", pubkey]) |
|---|
| 237 | # make the directory un-writable (so we can't create a new cert) |
|---|
| 238 | os.chmod("foo", 0o550) |
|---|
| 239 | result = self.runner.invoke(grid_manager, ["--config", "foo", "sign", "storage0", "42"]) |
|---|
| 240 | self.assertEqual(result.exit_code, 1) |
|---|
| 241 | self.assertIn( |
|---|
| 242 | "Permission denied", |
|---|
| 243 | result.output, |
|---|
| 244 | ) |
|---|
| 245 | |
|---|
| 246 | |
|---|
| 247 | class TahoeAddGridManagerCert(TestCase): |
|---|
| 248 | """ |
|---|
| 249 | Test `tahoe admin add-grid-manager-cert` subcommand |
|---|
| 250 | """ |
|---|
| 251 | |
|---|
| 252 | @inlineCallbacks |
|---|
| 253 | def test_help(self): |
|---|
| 254 | """ |
|---|
| 255 | some kind of help is printed |
|---|
| 256 | """ |
|---|
| 257 | code, out, err = yield run_cli("admin", "add-grid-manager-cert") |
|---|
| 258 | self.assertEqual(err, "") |
|---|
| 259 | self.assertNotEqual(0, code) |
|---|
| 260 | |
|---|
| 261 | @inlineCallbacks |
|---|
| 262 | def test_no_name(self): |
|---|
| 263 | """ |
|---|
| 264 | error to miss --name option |
|---|
| 265 | """ |
|---|
| 266 | code, out, err = yield run_cli( |
|---|
| 267 | "admin", "add-grid-manager-cert", "--filename", "-", |
|---|
| 268 | stdin=b"the cert", |
|---|
| 269 | ) |
|---|
| 270 | self.assertIn( |
|---|
| 271 | "Must provide --name", |
|---|
| 272 | out |
|---|
| 273 | ) |
|---|
| 274 | |
|---|
| 275 | @inlineCallbacks |
|---|
| 276 | def test_no_filename(self): |
|---|
| 277 | """ |
|---|
| 278 | error to miss --name option |
|---|
| 279 | """ |
|---|
| 280 | code, out, err = yield run_cli( |
|---|
| 281 | "admin", "add-grid-manager-cert", "--name", "foo", |
|---|
| 282 | stdin=b"the cert", |
|---|
| 283 | ) |
|---|
| 284 | self.assertIn( |
|---|
| 285 | "Must provide --filename", |
|---|
| 286 | out |
|---|
| 287 | ) |
|---|
| 288 | |
|---|
| 289 | @inlineCallbacks |
|---|
| 290 | def test_add_one(self): |
|---|
| 291 | """ |
|---|
| 292 | we can add a certificate |
|---|
| 293 | """ |
|---|
| 294 | nodedir = self.mktemp() |
|---|
| 295 | fake_cert = b"""{"certificate": "", "signature": ""}""" |
|---|
| 296 | |
|---|
| 297 | code, out, err = yield run_cli( |
|---|
| 298 | "--node-directory", nodedir, |
|---|
| 299 | "admin", "add-grid-manager-cert", "-f", "-", "--name", "foo", |
|---|
| 300 | stdin=fake_cert, |
|---|
| 301 | ignore_stderr=True, |
|---|
| 302 | ) |
|---|
| 303 | nodepath = FilePath(nodedir) |
|---|
| 304 | with nodepath.child("tahoe.cfg").open("r") as f: |
|---|
| 305 | config_data = f.read() |
|---|
| 306 | |
|---|
| 307 | self.assertIn("tahoe.cfg", nodepath.listdir()) |
|---|
| 308 | self.assertIn( |
|---|
| 309 | b"foo = foo.cert", |
|---|
| 310 | config_data, |
|---|
| 311 | ) |
|---|
| 312 | self.assertIn("foo.cert", nodepath.listdir()) |
|---|
| 313 | with nodepath.child("foo.cert").open("r") as f: |
|---|
| 314 | self.assertEqual( |
|---|
| 315 | json.load(f), |
|---|
| 316 | json.loads(fake_cert) |
|---|
| 317 | ) |
|---|