diff --git a/bindings/python/tests/fdbcli_tests.py b/bindings/python/tests/fdbcli_tests.py index bb2746df13..29ac474eff 100755 --- a/bindings/python/tests/fdbcli_tests.py +++ b/bindings/python/tests/fdbcli_tests.py @@ -542,6 +542,121 @@ def triggerddteaminfolog(logger): output = run_fdbcli_command('triggerddteaminfolog') assert output == 'Triggered team info logging in data distribution.' +@enable_logging() +def tenants(logger): + output = run_fdbcli_command('listtenants') + print(output) + assert output == 'The cluster has no tenants' + + output = run_fdbcli_command('createtenant tenant') + print(output) + assert output == 'The tenant `tenant\' has been created' + + output = run_fdbcli_command('createtenant tenant2') + print(output) + assert output == 'The tenant `tenant2\' has been created' + + output = run_fdbcli_command('listtenants') + print(output) + assert output == '1. tenant\n 2. tenant2' + + output = run_fdbcli_command('listtenants a z 1') + print(output) + assert output == '1. tenant' + + output = run_fdbcli_command('listtenants a tenant2') + print(output) + assert output == '1. tenant' + + output = run_fdbcli_command('listtenants tenant2 z') + print(output) + assert output == '1. tenant2' + + output = run_fdbcli_command('gettenant tenant') + print(output) + lines = output.split('\n') + assert len(lines) == 2 + assert lines[0].strip().startswith('id: ') + assert lines[1].strip().startswith('prefix: ') + + output = run_fdbcli_command('usetenant') + print(output) + assert output == 'Using the default tenant' + + output = run_fdbcli_command_and_get_error('usetenant tenant3') + print(output) + assert output == 'ERROR: Tenant `tenant3\' does not exist' + + # Test writing keys to different tenants and make sure they all work correctly + run_fdbcli_command('writemode on; set tenant_test default_tenant') + output = run_fdbcli_command('get tenant_test') + print(output) + assert output == '`tenant_test\' is `default_tenant\'' + + process = subprocess.Popen(command_template[:-1], stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=fdbcli_env) + cmd_sequence = ['writemode on', 'usetenant tenant', 'get tenant_test', 'set tenant_test tenant'] + output, _ = process.communicate(input='\n'.join(cmd_sequence).encode()) + + lines = output.decode().strip().split('\n')[-3:] + print(lines) + assert lines[0] == 'Using tenant `tenant\'' + assert lines[1] == '`tenant_test\': not found' + assert lines[2].startswith('Committed') + + process = subprocess.Popen(command_template[:-1], stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=fdbcli_env) + cmd_sequence = ['writemode on', 'usetenant tenant2', 'get tenant_test', 'set tenant_test tenant2', 'get tenant_test'] + output, _ = process.communicate(input='\n'.join(cmd_sequence).encode()) + + lines = output.decode().strip().split('\n')[-4:] + print(lines) + assert lines[0] == 'Using tenant `tenant2\'' + assert lines[1] == '`tenant_test\': not found' + assert lines[2].startswith('Committed') + assert lines[3] == '`tenant_test\' is `tenant2\'' + + process = subprocess.Popen(command_template[:-1], stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=fdbcli_env) + cmd_sequence = ['usetenant tenant', 'get tenant_test', 'defaulttenant', 'get tenant_test'] + output, _ = process.communicate(input='\n'.join(cmd_sequence).encode()) + + lines = output.decode().strip().split('\n')[-4:] + print(lines) + assert lines[0] == 'Using tenant `tenant\'' + assert lines[1] == '`tenant_test\' is `tenant\'' + assert lines[2] == 'Using the default tenant' + assert lines[3] == '`tenant_test\' is `default_tenant\'' + + process = subprocess.Popen(command_template[:-1], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=fdbcli_env) + cmd_sequence = ['writemode on', 'usetenant tenant', 'clear tenant_test', 'deletetenant tenant', 'get tenant_test', 'defaulttenant', 'usetenant tenant'] + output, error_output = process.communicate(input='\n'.join(cmd_sequence).encode()) + + lines = output.decode().strip().split('\n')[-7:] + error_lines = error_output.decode().strip().split('\n')[-2:] + print(lines) + print(error_lines) + assert lines[0] == 'Using tenant `tenant\'' + assert lines[1].startswith('Committed') + assert lines[2] == 'The tenant `tenant\' has been deleted' + assert lines[3] == 'WARNING: the active tenant was deleted. Use the `usetenant\' or `defaulttenant\'' + assert lines[4] == 'command to choose a new tenant.' + assert error_lines[0] == 'ERROR: Tenant does not exist (2131)' + assert lines[6] == 'Using the default tenant' + assert error_lines[1] == 'ERROR: Tenant `tenant\' does not exist' + + process = subprocess.Popen(command_template[:-1], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=fdbcli_env) + cmd_sequence = ['writemode on', 'deletetenant tenant2', 'usetenant tenant2', 'clear tenant_test', 'defaulttenant', 'deletetenant tenant2'] + output, error_output = process.communicate(input='\n'.join(cmd_sequence).encode()) + + lines = output.decode().strip().split('\n')[-4:] + error_lines = error_output.decode().strip().split('\n')[-1:] + print(lines) + print(error_lines) + assert error_lines[0] == 'ERROR: Cannot delete a non-empty tenant (2133)' + assert lines[0] == 'Using tenant `tenant2\'' + assert lines[1].startswith('Committed') + assert lines[2] == 'Using the default tenant' + assert lines[3] == 'The tenant `tenant2\' has been deleted' + + run_fdbcli_command('writemode on; clear tenant_test') if __name__ == '__main__': parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, @@ -586,6 +701,7 @@ if __name__ == '__main__': transaction() throttle() triggerddteaminfolog() + tenants() else: assert args.process_number > 1, "Process number should be positive" coordinators() diff --git a/documentation/sphinx/source/command-line-interface.rst b/documentation/sphinx/source/command-line-interface.rst index 6d2243f02f..72d7da54ef 100644 --- a/documentation/sphinx/source/command-line-interface.rst +++ b/documentation/sphinx/source/command-line-interface.rst @@ -153,6 +153,27 @@ If ``description=`` is specified, the description field in the cluster fil For more information on setting the cluster description, see :ref:`configuration-setting-cluster-description`. +createtenant +------------ + +The ``createtenant`` command is used to create new tenants in the cluster. Its syntax is ``createtenant ``. + +The tenant name can be any byte string that does not begin with the ``\xff`` byte. If the tenant already exists, ``fdbcli`` will report an error. + +defaulttenant +------------- + +The ``defaulttenant`` command configures ``fdbcli`` to run its commands without a tenant. This is the default behavior. + +The active tenant cannot be changed while a transaction (using ``begin``) is open. + +deletetenant +------------ + +The ``deletetenant`` command is used to delete tenants from the cluster. Its syntax is ``deletetenant ``. + +In order to delete a tenant, it must be empty. To delete a tenant with data, first clear that data using the ``clear`` command. If the tenant does not exist, ``fdbcli`` will report an error. + exclude ------- @@ -210,6 +231,13 @@ The ``getrangekeys`` command fetches keys in a range. Its syntax is ``getrangeke Note that :ref:`characters can be escaped ` when specifying keys (or values) in ``fdbcli``. +gettenant +--------- + +The ``gettenant`` command fetches metadata for a given tenant and displays it. Its syntax is ``gettenant ``. + +Included in the output of this command are the ``id`` and ``prefix`` assigned to the tenant. If the tenant does not exist, ``fdbcli`` will report an error. + getversion ---------- @@ -300,6 +328,13 @@ Attempts to kill all specified processes. Each address should include the IP and Attempts to kill all known processes in the cluster. +listtenants +----------- + +The ``listtenants`` command prints the names of tenants in the cluster. Its syntax is ``listtenants [BEGIN] [END] [LIMIT]``. + +By default, the ``listtenants`` command will print up to 100 entries from the entire range of tenants. A narrower sub-range can be printed using the optional ``[BEGIN]`` and ``[END]`` parameters, and the limit can be changed by specifying an integer ``[LIMIT]`` parameter. + lock ---- @@ -512,6 +547,17 @@ unlock The ``unlock`` command unlocks the database with the specified lock UID. Because this is a potentially dangerous operation, users must copy a passphrase before the unlock command is executed. +usetenant +--------- + +The ``usetenant`` command configures ``fdbcli`` to run transactions within the specified tenant. Its syntax is ``usetenant ``. + +When configured, transactions will read and write keys from the key-space associated with the specified tenant. By default, ``fdbcli`` runs without a tenant. Management operations that modify keys (e.g. ``exclude``) will not operate within the tenant. + +If the tenant chosen does not exist, ``fdbcli`` will report an error. + +The active tenant cannot be changed while a transaction (using ``begin``) is open. + writemode --------- diff --git a/fdbcli/TenantCommands.actor.cpp b/fdbcli/TenantCommands.actor.cpp index b69ea8ebde..c03bb17c88 100644 --- a/fdbcli/TenantCommands.actor.cpp +++ b/fdbcli/TenantCommands.actor.cpp @@ -72,7 +72,7 @@ ACTOR Future createTenantCommandActor(Reference db, std::vector } } - printf("The tenant `%s' has been created.\n", printable(tokens[1]).c_str()); + printf("The tenant `%s' has been created\n", printable(tokens[1]).c_str()); return true; } @@ -117,7 +117,7 @@ ACTOR Future deleteTenantCommandActor(Reference db, std::vector } } - printf("The tenant `%s' has been deleted.\n", printable(tokens[1]).c_str()); + printf("The tenant `%s' has been deleted\n", printable(tokens[1]).c_str()); return true; } @@ -151,7 +151,7 @@ ACTOR Future listTenantsCommandActor(Reference db, std::vector< } if (tokens.size() == 4) { int n = 0; - if (sscanf(tokens[3].toString().c_str(), "%d%n", &limit, &n) != 1 || n != tokens[4].size()) { + if (sscanf(tokens[3].toString().c_str(), "%d%n", &limit, &n) != 1 || n != tokens[3].size()) { fprintf(stderr, "ERROR: invalid limit %s\n", tokens[3].toString().c_str()); return false; } @@ -168,9 +168,9 @@ ACTOR Future listTenantsCommandActor(Reference db, std::vector< if (tenants.empty()) { if (tokens.size() == 1) { - printf("The cluster has no tenants.\n"); + printf("The cluster has no tenants\n"); } else { - printf("The cluster has no tenants in the specified range.\n"); + printf("The cluster has no tenants in the specified range\n"); } } @@ -218,7 +218,17 @@ ACTOR Future getTenantCommandActor(Reference db, std::vector cli(CLIOptions opt, LineNoise* plinenoise) { bool _result = wait(makeInterruptable(deleteTenantCommandActor(db, tokens))); if (!_result) is_error = true; + else if (tenantName.present() && tokens[1] == tenantName.get()) { + printAtCol("WARNING: the active tenant was deleted. Use the `usetenant' or `defaulttenant' " + "command to choose a new tenant.\n", + 80); + } continue; } diff --git a/tests/TestRunner/local_cluster.py b/tests/TestRunner/local_cluster.py index 1f733aa4d6..30162147fe 100644 --- a/tests/TestRunner/local_cluster.py +++ b/tests/TestRunner/local_cluster.py @@ -136,5 +136,5 @@ logdir = {logdir} def create_database(self, storage='ssd'): args = [self.fdbcli_binary, '-C', self.etc.joinpath('fdb.cluster'), '--exec', - 'configure new single {}'.format(storage)] + 'configure new single {} tenant_mode=optional_experimental'.format(storage)] subprocess.run(args)