From b82478e1061bfc1475693b0910a7f14276b4d6ab Mon Sep 17 00:00:00 2001 From: =?utf8?q?Alfonso=20Mart=C3=ADnez?= Date: Tue, 15 Dec 2020 09:28:46 +0100 Subject: [PATCH] mgr/dashboard: CLI commands: read passwords from file MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Fixes: https://tracker.ceph.com/issues/48355 Signed-off-by: Alfonso Martínez Signed-off-by: Juan Miguel Olmo Martínez (cherry picked from commit 5d7ee7c1f0ad971fd0079f917e2b44cdef1d6f9f) Conflicts: doc/mgr/dashboard.rst qa/tasks/mgr/dashboard/helper.py qa/tasks/mgr/dashboard/test_auth.py qa/tasks/mgr/dashboard/test_rgw.py qa/tasks/mgr/dashboard/test_user.py qa/workunits/cephadm/test_dashboard_e2e.sh src/cephadm/cephadm src/pybind/mgr/cephadm/services/cephadmservice.py src/pybind/mgr/cephadm/services/iscsi.py src/pybind/mgr/cephadm/tests/test_cephadm.py src/pybind/mgr/cephadm/tests/test_services.py src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh src/pybind/mgr/dashboard/services/access_control.py src/pybind/mgr/dashboard/services/iscsi_cli.py src/pybind/mgr/dashboard/tests/test_access_control.py src/pybind/mgr/dashboard/tests/test_iscsi.py src/pybind/mgr/dashboard/tests/test_settings.py src/pybind/mgr/mgr_module.py src/pybind/mgr/tests/__init__.py src/test/mgr/mgr-dashboard-smoke.sh src/vstart.sh - Remove cephadm files and related code (does not apply to nautilus). - Remove code related to non-existing functionality in nautilus. - Adapt code to be py2 compatible. - Resolve conflicts related to code divergence. --- doc/mgr/dashboard.rst | 19 ++++---- qa/tasks/mgr/dashboard/helper.py | 47 +++++++++++++++---- qa/tasks/mgr/dashboard/test_auth.py | 19 +++----- qa/tasks/mgr/dashboard/test_ganesha.py | 4 +- qa/tasks/mgr/dashboard/test_rgw.py | 12 ++--- src/pybind/mgr/dashboard/module.py | 2 +- .../mgr/dashboard/run-frontend-e2e-tests.sh | 8 +++- .../mgr/dashboard/services/access_control.py | 27 ++++++----- .../mgr/dashboard/services/iscsi_cli.py | 10 ++-- src/pybind/mgr/dashboard/settings.py | 29 ++++++++++-- src/pybind/mgr/dashboard/tests/__init__.py | 4 +- .../dashboard/tests/test_access_control.py | 26 ++++++---- src/pybind/mgr/dashboard/tests/test_iscsi.py | 20 +++++--- .../mgr/dashboard/tests/test_settings.py | 44 ++++++++++++++--- src/pybind/mgr/mgr_module.py | 46 ++++++++++++++++-- src/test/mgr/mgr-dashboard-smoke.sh | 5 +- src/vstart.sh | 17 ++++--- 17 files changed, 242 insertions(+), 97 deletions(-) diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index 206a04787455d..5d14be6c91cf8 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -256,7 +256,7 @@ section. To create a user with the administrator role you can use the following commands:: - $ ceph dashboard ac-user-create administrator + $ ceph dashboard ac-user-create -i administrator .. _dashboard-enabling-object-gateway: @@ -283,8 +283,8 @@ The credentials of an existing user can also be obtained by using Finally, provide the credentials to the dashboard:: - $ ceph dashboard set-rgw-api-access-key - $ ceph dashboard set-rgw-api-secret-key + $ ceph dashboard set-rgw-api-access-key -i + $ ceph dashboard set-rgw-api-secret-key -i In a typical default configuration with a single RGW endpoint, this is all you have to do to get the Object Gateway management functionality working. The @@ -344,9 +344,10 @@ To disable API SSL verification run the following commmand:: The available iSCSI gateways must be defined using the following commands:: - $ ceph dashboard iscsi-gateway-list - $ ceph dashboard iscsi-gateway-add ://:@[:port] - $ ceph dashboard iscsi-gateway-rm + $ ceph dashboard iscsi-gateway-list + $ # Gateway URL format for a new gateway: ://:@[:port] + $ ceph dashboard iscsi-gateway-add -i [] + $ ceph dashboard iscsi-gateway-rm .. _dashboard-grafana: @@ -646,7 +647,7 @@ We provide a set of CLI commands to manage user accounts: - *Create User*:: - $ ceph dashboard ac-user-create [] [] [] [] + $ ceph dashboard ac-user-create -i [] [] [] - *Delete User*:: @@ -654,7 +655,7 @@ We provide a set of CLI commands to manage user accounts: - *Change Password*:: - $ ceph dashboard ac-user-set-password + $ ceph dashboard ac-user-set-password -i - *Modify User (name, and email)*:: @@ -781,7 +782,7 @@ view and create Ceph pools, and have read-only access to any other scopes. 1. *Create the user*:: - $ ceph dashboard ac-user-create bob mypassword + $ ceph dashboard ac-user-create bob -i 2. *Create role and specify scope permissions*:: diff --git a/qa/tasks/mgr/dashboard/helper.py b/qa/tasks/mgr/dashboard/helper.py index 9627a8428f84e..d7e6accee0b81 100644 --- a/qa/tasks/mgr/dashboard/helper.py +++ b/qa/tasks/mgr/dashboard/helper.py @@ -4,6 +4,8 @@ from __future__ import absolute_import import json import logging +import random +import string from collections import namedtuple import time @@ -49,7 +51,10 @@ class DashboardTestCase(MgrTestCase): if ex.exitstatus != 2: raise ex - cls._ceph_cmd(['dashboard', 'ac-user-create', username, password]) + user_create_args = [ + 'dashboard', 'ac-user-create', username + ] + cls._ceph_cmd_with_secret(user_create_args, password) set_roles_args = ['dashboard', 'ac-user-set-roles', username] for idx, role in enumerate(roles): @@ -375,33 +380,55 @@ class DashboardTestCase(MgrTestCase): log.info("command result: %s", res) return res + @classmethod + def _ceph_cmd_result(cls, cmd): + exitstatus = cls.mgr_cluster.mon_manager.raw_cluster_cmd_result(*cmd) + log.info("command exit status: %d", exitstatus) + return exitstatus + + @classmethod + def _ceph_cmd_with_secret(cls, cmd, secret, return_exit_code=False): + cmd.append('-i') + cmd.append('{}'.format(cls._ceph_create_tmp_file(secret))) + if return_exit_code: + return cls._ceph_cmd_result(cmd) + return cls._ceph_cmd(cmd) + + @classmethod + def _ceph_create_tmp_file(cls, content): + """Create a temporary file in the remote cluster""" + file_name = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20)) + file_path = '/tmp/{}'.format(file_name) + cls._cmd(['sh', '-c', 'echo -n {} > {}'.format(content, file_path)]) + return file_path + def set_config_key(self, key, value): self._ceph_cmd(['config-key', 'set', key, value]) def get_config_key(self, key): return self._ceph_cmd(['config-key', 'get', key]) + @classmethod + def _cmd(cls, args): + return cls.mgr_cluster.admin_remote.run(args=args) + @classmethod def _rbd_cmd(cls, cmd): - args = [ - 'rbd' - ] + args = ['rbd'] args.extend(cmd) - cls.mgr_cluster.admin_remote.run(args=args) + cls._cmd(args) @classmethod def _radosgw_admin_cmd(cls, cmd): - args = [ - 'radosgw-admin' - ] + args = ['radosgw-admin'] args.extend(cmd) - cls.mgr_cluster.admin_remote.run(args=args) + cls._cmd(args) @classmethod def _rados_cmd(cls, cmd): args = ['rados'] args.extend(cmd) - cls.mgr_cluster.admin_remote.run(args=args) + cls._cmd(args) @classmethod def mons(cls): diff --git a/qa/tasks/mgr/dashboard/test_auth.py b/qa/tasks/mgr/dashboard/test_auth.py index 0acc64478d17d..6603b85928ba2 100644 --- a/qa/tasks/mgr/dashboard/test_auth.py +++ b/qa/tasks/mgr/dashboard/test_auth.py @@ -5,6 +5,8 @@ from __future__ import absolute_import import time import jwt +from teuthology.orchestra.run import \ + CommandFailedError # pylint: disable=import-error from .helper import DashboardTestCase @@ -29,6 +31,10 @@ class AuthTest(DashboardTestCase): self.assertIn('create', perms) self.assertIn('delete', perms) + def test_login_without_password(self): + with self.assertRaises(CommandFailedError): + self.create_user('admin2', '', ['administrator'], force_password=True) + def test_a_set_login_credentials(self): self.create_user('admin2', 'admin2', ['administrator']) self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'}) @@ -52,17 +58,6 @@ class AuthTest(DashboardTestCase): "detail": "Invalid credentials" }) - def test_login_without_password(self): - self.create_user('admin2', '', ['administrator']) - self._post("/api/auth", {'username': 'admin2', 'password': ''}) - self.assertStatus(400) - self.assertJsonBody({ - "component": "auth", - "code": "invalid_credentials", - "detail": "Invalid credentials" - }) - self.delete_user('admin2') - def test_logout(self): self._post("/api/auth", {'username': 'admin', 'password': 'admin'}) self.assertStatus(201) @@ -126,7 +121,7 @@ class AuthTest(DashboardTestCase): self._get("/api/host") self.assertStatus(200) time.sleep(1) - self._ceph_cmd(['dashboard', 'ac-user-set-password', 'user', 'user2']) + self._ceph_cmd_with_secret(['dashboard', 'ac-user-set-password', 'user'], 'user2') time.sleep(1) self._get("/api/host") self.assertStatus(401) diff --git a/qa/tasks/mgr/dashboard/test_ganesha.py b/qa/tasks/mgr/dashboard/test_ganesha.py index cd869a00e405b..b90bb4afcfeb8 100644 --- a/qa/tasks/mgr/dashboard/test_ganesha.py +++ b/qa/tasks/mgr/dashboard/test_ganesha.py @@ -40,8 +40,8 @@ class GaneshaTest(DashboardTestCase): 'user', 'create', '--uid', 'admin', '--display-name', 'admin', '--system', '--access-key', 'admin', '--secret', 'admin' ]) - cls._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin']) - cls._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin']) + cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin') + cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin') @classmethod def tearDownClass(cls): diff --git a/qa/tasks/mgr/dashboard/test_rgw.py b/qa/tasks/mgr/dashboard/test_rgw.py index dadec1091634e..64c185291d3dc 100644 --- a/qa/tasks/mgr/dashboard/test_rgw.py +++ b/qa/tasks/mgr/dashboard/test_rgw.py @@ -26,8 +26,8 @@ class RgwTestCase(DashboardTestCase): '--system', '--access-key', 'admin', '--secret', 'admin' ]) # Update the dashboard configuration. - cls._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin']) - cls._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin']) + cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin') + cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin') # Create a test user? if cls.create_test_user: cls._radosgw_admin_cmd([ @@ -75,13 +75,13 @@ class RgwApiCredentialsTest(RgwTestCase): self._ceph_cmd(['mgr', 'module', 'enable', 'dashboard', '--force']) # Set the default credentials. self._ceph_cmd(['dashboard', 'set-rgw-api-user-id', '']) - self._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin']) - self._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin']) + self._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin') + self._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin') super(RgwApiCredentialsTest, self).setUp() def test_no_access_secret_key(self): - self._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', '']) - self._ceph_cmd(['dashboard', 'set-rgw-api-access-key', '']) + self._ceph_cmd(['dashboard', 'reset-rgw-api-secret-key']) + self._ceph_cmd(['dashboard', 'reset-rgw-api-access-key']) resp = self._get('/api/rgw/user') self.assertStatus(500) self.assertIn('detail', resp) diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index ab78c8c697b2f..ee34e49cc344e 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -384,7 +384,7 @@ class Module(MgrModule, CherryPyConfig): def handle_command(self, inbuf, cmd): # pylint: disable=too-many-return-statements - res = handle_option_command(cmd) + res = handle_option_command(cmd, inbuf) if res[0] != -errno.ENOSYS: return res res = handle_sso_command(cmd) diff --git a/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh b/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh index 87a1e6b4697b4..25e2963450e54 100755 --- a/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh +++ b/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh @@ -51,8 +51,12 @@ if [ "$BASE_URL" == "" ]; then # Set the user-id ./bin/ceph dashboard set-rgw-api-user-id dev # Obtain and set access and secret key for the previously created user - ./bin/ceph dashboard set-rgw-api-access-key `./bin/radosgw-admin user info --uid=dev | jq .keys[0].access_key | sed -e 's/^"//' -e 's/"$//'` - ./bin/ceph dashboard set-rgw-api-secret-key `./bin/radosgw-admin user info --uid=dev | jq .keys[0].secret_key | sed -e 's/^"//' -e 's/"$//'` + RGW_ACCESS_KEY_FILE="/tmp/rgw-user-access-key.txt" + printf "$(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].access_key)" > "${RGW_ACCESS_KEY_FILE}" + ./bin/ceph dashboard set-rgw-api-access-key -i "${RGW_ACCESS_KEY_FILE}" + RGW_SECRET_KEY_FILE="/tmp/rgw-user-secret-key.txt" + printf "$(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].secret_key)" > "${RGW_SECRET_KEY_FILE}" + ./bin/ceph dashboard set-rgw-api-secret-key -i "${RGW_SECRET_KEY_FILE}" # Set SSL verify to False ./bin/ceph dashboard set-rgw-api-ssl-verify False diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index 3787be62d482d..16a31285f49b5 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -10,7 +10,7 @@ import time import bcrypt -from mgr_module import CLIReadCommand, CLIWriteCommand +from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand from .. import mgr, logger from ..security import Scope, Permission @@ -367,10 +367,11 @@ def load_access_control_db(): # CLI dashboard access control scope commands @CLIWriteCommand('dashboard set-login-credentials', - 'name=username,type=CephString ' - 'name=password,type=CephString', - 'Set the login credentials') -def set_login_credentials_cmd(_, username, password): + 'name=username,type=CephString', + 'Set the login credentials. Password read from -i ') +@CLICheckNonemptyFileInput +def set_login_credentials_cmd(_, username, inbuf): + password = inbuf try: user = mgr.ACCESS_CTRL_DB.get_user(username) user.set_password(password) @@ -500,13 +501,14 @@ def ac_user_show_cmd(_, username=None): @CLIWriteCommand('dashboard ac-user-create', 'name=username,type=CephString ' - 'name=password,type=CephString,req=false ' 'name=rolename,type=CephString,req=false ' 'name=name,type=CephString,req=false ' 'name=email,type=CephString,req=false', - 'Create a user') -def ac_user_create_cmd(_, username, password=None, rolename=None, name=None, + 'Create a user. Password read from -i ') +@CLICheckNonemptyFileInput +def ac_user_create_cmd(_, username, inbuf, rolename=None, name=None, email=None): + password = inbuf try: role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None except RoleDoesNotExist as ex: @@ -609,10 +611,11 @@ def ac_user_del_roles_cmd(_, username, roles): @CLIWriteCommand('dashboard ac-user-set-password', - 'name=username,type=CephString ' - 'name=password,type=CephString', - 'Set user password') -def ac_user_set_password(_, username, password): + 'name=username,type=CephString', + 'Set user password from -i ') +@CLICheckNonemptyFileInput +def ac_user_set_password(_, username, inbuf): + password = inbuf try: user = mgr.ACCESS_CTRL_DB.get_user(username) user.set_password(password) diff --git a/src/pybind/mgr/dashboard/services/iscsi_cli.py b/src/pybind/mgr/dashboard/services/iscsi_cli.py index fca1f61b3cc14..900c79af43d0e 100644 --- a/src/pybind/mgr/dashboard/services/iscsi_cli.py +++ b/src/pybind/mgr/dashboard/services/iscsi_cli.py @@ -4,7 +4,7 @@ from __future__ import absolute_import import errno import json -from mgr_module import CLIReadCommand, CLIWriteCommand +from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand from .iscsi_client import IscsiClient from .iscsi_config import IscsiGatewaysConfig, IscsiGatewayAlreadyExists, InvalidServiceUrl, \ @@ -18,9 +18,11 @@ def list_iscsi_gateways(_): @CLIWriteCommand('dashboard iscsi-gateway-add', - 'name=service_url,type=CephString', - 'Add iSCSI gateway configuration') -def add_iscsi_gateway(_, service_url): + None, + 'Add iSCSI gateway configuration. Gateway URL read from -i ') +@CLICheckNonemptyFileInput +def add_iscsi_gateway(_, inbuf): + service_url = inbuf try: IscsiGatewaysConfig.validate_service_url(service_url) name = IscsiClient.instance(service_url=service_url).get_hostname()['data'] diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py index 31e09fac5ed21..d9936b5008d4c 100644 --- a/src/pybind/mgr/dashboard/settings.py +++ b/src/pybind/mgr/dashboard/settings.py @@ -5,6 +5,8 @@ import errno import inspect from six import add_metaclass +from mgr_module import CLICheckNonemptyFileInput + from . import mgr @@ -127,12 +129,16 @@ def options_command_list(): 'perm': 'r' }) elif cmd.startswith('dashboard set'): - cmd_list.append({ + cmd_entry = { 'cmd': '{} name=value,type={}' .format(cmd, py2ceph(opt['type'])), 'desc': 'Set the {} option value'.format(opt['name']), 'perm': 'w' - }) + } + if handles_secret(cmd): + cmd_entry['cmd'] = cmd + cmd_entry['desc'] = '{} read from -i '.format(cmd_entry['desc']) + cmd_list.append(cmd_entry) elif cmd.startswith('dashboard reset'): desc = 'Reset the {} option to its default value'.format( opt['name']) @@ -159,7 +165,7 @@ def options_schema_list(): return result -def handle_option_command(cmd): +def handle_option_command(cmd, inbuf): if cmd['prefix'] not in _OPTIONS_COMMAND_MAP: return -errno.ENOSYS, '', "Command not found '{}'".format(cmd['prefix']) @@ -172,8 +178,23 @@ def handle_option_command(cmd): elif cmd['prefix'].startswith('dashboard get'): return 0, str(getattr(Settings, opt['name'])), '' elif cmd['prefix'].startswith('dashboard set'): - value = opt['type'](cmd['value']) + if handles_secret(cmd['prefix']): + value, stdout, stderr = get_secret(inbuf=inbuf) + if stderr: + return value, stdout, stderr + else: + value = cmd['value'] + value = opt['type'](value) if opt['type'] == bool and cmd['value'].lower() == 'false': value = False setattr(Settings, opt['name'], value) return 0, 'Option {} updated'.format(opt['name']), '' + + +def handles_secret(cmd): + return bool([cmd for secret_word in ['password', 'key'] if (secret_word in cmd)]) + + +@CLICheckNonemptyFileInput +def get_secret(inbuf=None): + return inbuf, None, None diff --git a/src/pybind/mgr/dashboard/tests/__init__.py b/src/pybind/mgr/dashboard/tests/__init__.py index 449b4245ec8b5..589986903b705 100644 --- a/src/pybind/mgr/dashboard/tests/__init__.py +++ b/src/pybind/mgr/dashboard/tests/__init__.py @@ -34,6 +34,7 @@ class CmdException(Exception): def exec_dashboard_cmd(command_handler, cmd, **kwargs): + inbuf = kwargs['inbuf'] if 'inbuf' in kwargs else None cmd_dict = {'prefix': 'dashboard {}'.format(cmd)} cmd_dict.update(kwargs) if cmd_dict['prefix'] not in CLICommand.COMMANDS: @@ -45,8 +46,7 @@ def exec_dashboard_cmd(command_handler, cmd, **kwargs): except ValueError: return out - ret, out, err = CLICommand.COMMANDS[cmd_dict['prefix']].call(mgr, cmd_dict, - None) + ret, out, err = CLICommand.COMMANDS[cmd_dict['prefix']].call(mgr, cmd_dict, inbuf) if ret < 0: raise CmdException(ret, err) try: diff --git a/src/pybind/mgr/dashboard/tests/test_access_control.py b/src/pybind/mgr/dashboard/tests/test_access_control.py index 68bdaf1aa79f7..268a4f1861e9b 100644 --- a/src/pybind/mgr/dashboard/tests/test_access_control.py +++ b/src/pybind/mgr/dashboard/tests/test_access_control.py @@ -7,6 +7,8 @@ import json import time import unittest +from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE + from . import CmdException, CLICommandTestMixin from .. import mgr from ..security import Scope, Permission @@ -271,7 +273,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): def test_create_user(self, username='admin', rolename=None): user = self.exec_cmd('ac-user-create', username=username, - rolename=rolename, password='admin', + rolename=rolename, inbuf='admin', name='{} User'.format(username), email='{}@user.com'.format(username)) @@ -309,7 +311,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): self.test_create_user() with self.assertRaises(CmdException) as ctx: - self.exec_cmd('ac-user-create', username='admin', password='admin') + self.exec_cmd('ac-user-create', username='admin', inbuf='admin') self.assertEqual(ctx.exception.retcode, -errno.EEXIST) self.assertEqual(str(ctx.exception), "User 'admin' already exists") @@ -321,7 +323,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): # create a user with a role that does not exist; expect a failure try: self.exec_cmd('ac-user-create', username='foo', - rolename='dne_role', password='foopass', + rolename='dne_role', inbuf='foopass', name='foo User', email='foo@user.com') except CmdException as e: self.assertEqual(e.retcode, -errno.ENOENT) @@ -341,7 +343,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): # create a role (this will be 'test_role') self.test_create_role() self.exec_cmd('ac-user-create', username='bar', - rolename='test_role', password='barpass', + rolename='test_role', inbuf='barpass', name='bar User', email='bar@user.com') # validate db: @@ -540,7 +542,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): def test_set_user_password(self): user_orig = self.test_create_user() user = self.exec_cmd('ac-user-set-password', username='admin', - password='newpass') + inbuf='newpass') pass_hash = password_hash('newpass', user['password']) self.assertDictEqual(user, { 'username': 'admin', @@ -557,14 +559,22 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): def test_set_user_password_nonexistent_user(self): with self.assertRaises(CmdException) as ctx: self.exec_cmd('ac-user-set-password', username='admin', - password='newpass') + inbuf='newpass') self.assertEqual(ctx.exception.retcode, -errno.ENOENT) self.assertEqual(str(ctx.exception), "User 'admin' does not exist") + def test_set_user_password_empty(self): + with self.assertRaises(CmdException) as ctx: + self.exec_cmd('ac-user-set-password', username='admin', inbuf='\n', + force_password=True) + + self.assertEqual(ctx.exception.retcode, -errno.EINVAL) + self.assertEqual(str(ctx.exception), ERROR_MSG_EMPTY_INPUT_FILE) + def test_set_login_credentials(self): self.exec_cmd('set-login-credentials', username='admin', - password='admin') + inbuf='admin') user = self.exec_cmd('ac-user-show', username='admin') pass_hash = password_hash('admin', user['password']) self.assertDictEqual(user, { @@ -581,7 +591,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): def test_set_login_credentials_for_existing_user(self): self.test_add_user_roles('admin', ['read-only']) self.exec_cmd('set-login-credentials', username='admin', - password='admin2') + inbuf='admin2') user = self.exec_cmd('ac-user-show', username='admin') pass_hash = password_hash('admin2', user['password']) self.assertDictEqual(user, { diff --git a/src/pybind/mgr/dashboard/tests/test_iscsi.py b/src/pybind/mgr/dashboard/tests/test_iscsi.py index 4a6540f52def0..34d2f014a9c11 100644 --- a/src/pybind/mgr/dashboard/tests/test_iscsi.py +++ b/src/pybind/mgr/dashboard/tests/test_iscsi.py @@ -1,4 +1,4 @@ -# pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods, too-many-lines import copy import errno @@ -10,6 +10,8 @@ try: except ImportError: import unittest.mock as mock +from mgr_module import ERROR_MSG_NO_INPUT_FILE + from . import CmdException, ControllerTestCase, CLICommandTestMixin, KVStoreMockMixin from .. import mgr from ..controllers.iscsi import Iscsi, IscsiTarget @@ -28,19 +30,23 @@ class IscsiTestCli(unittest.TestCase, CLICommandTestMixin): def test_cli_add_gateway_invalid_url(self): with self.assertRaises(CmdException) as ctx: - self.exec_cmd('iscsi-gateway-add', name='node1', - service_url='http:/hello.com') + self.exec_cmd('iscsi-gateway-add', inbuf='http:/hello.com') self.assertEqual(ctx.exception.retcode, -errno.EINVAL) self.assertEqual(str(ctx.exception), "Invalid service URL 'http:/hello.com'. Valid format: " "'://:@[:port]'.") + def test_cli_add_gateway_empty_url(self): + with self.assertRaises(CmdException) as ctx: + self.exec_cmd('iscsi-gateway-add', inbuf='') + + self.assertEqual(ctx.exception.retcode, -errno.EINVAL) + self.assertEqual(str(ctx.exception), ERROR_MSG_NO_INPUT_FILE) + def test_cli_add_gateway(self): - self.exec_cmd('iscsi-gateway-add', name='node1', - service_url='https://admin:admin@10.17.5.1:5001') - self.exec_cmd('iscsi-gateway-add', name='node2', - service_url='https://admin:admin@10.17.5.2:5001') + self.exec_cmd('iscsi-gateway-add', inbuf='https://admin:admin@10.17.5.1:5001') + self.exec_cmd('iscsi-gateway-add', inbuf='https://admin:admin@10.17.5.2:5001') iscsi_config = json.loads(self.get_key("_iscsi_config")) self.assertEqual(iscsi_config['gateways'], { 'node1': { diff --git a/src/pybind/mgr/dashboard/tests/test_settings.py b/src/pybind/mgr/dashboard/tests/test_settings.py index aac785b7ce03b..2d0504297c78b 100644 --- a/src/pybind/mgr/dashboard/tests/test_settings.py +++ b/src/pybind/mgr/dashboard/tests/test_settings.py @@ -3,6 +3,9 @@ from __future__ import absolute_import import errno import unittest + +from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE + from . import KVStoreMockMixin, ControllerTestCase from .. import settings from ..controllers.settings import Settings as SettingsController @@ -42,7 +45,9 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin): def test_get_cmd(self): r, out, err = handle_option_command( - {'prefix': 'dashboard get-grafana-api-port'}) + {'prefix': 'dashboard get-grafana-api-port'}, + None + ) self.assertEqual(r, 0) self.assertEqual(out, '3000') self.assertEqual(err, '') @@ -50,14 +55,35 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin): def test_set_cmd(self): r, out, err = handle_option_command( {'prefix': 'dashboard set-grafana-api-port', - 'value': '4000'}) + 'value': '4000'}, + None + ) self.assertEqual(r, 0) self.assertEqual(out, 'Option GRAFANA_API_PORT updated') self.assertEqual(err, '') + def test_set_secret_empty(self): + r, out, err = handle_option_command( + {'prefix': 'dashboard set-rgw-api-secret-key'}, + None + ) + self.assertEqual(r, -errno.EINVAL) + self.assertEqual(out, '') + self.assertEqual(err, ERROR_MSG_EMPTY_INPUT_FILE) + + def test_set_secret(self): + r, out, err = handle_option_command( + {'prefix': 'dashboard set-rgw-api-secret-key'}, + 'my-secret' + ) + self.assertEqual(r, 0) + self.assertEqual(out, 'Option RGW_API_SECRET_KEY updated') + self.assertEqual(err, '') + def test_reset_cmd(self): r, out, err = handle_option_command( - {'prefix': 'dashboard reset-grafana-enabled'} + {'prefix': 'dashboard reset-grafana-enabled'}, + None ) self.assertEqual(r, 0) self.assertEqual(out, 'Option {} reset to default value "{}"'.format( @@ -66,7 +92,9 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin): def test_inv_cmd(self): r, out, err = handle_option_command( - {'prefix': 'dashboard get-non-existent-option'}) + {'prefix': 'dashboard get-non-existent-option'}, + None + ) self.assertEqual(r, -errno.ENOSYS) self.assertEqual(out, '') self.assertEqual(err, "Command not found " @@ -75,13 +103,17 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin): def test_sync(self): Settings.GRAFANA_API_PORT = 5000 r, out, err = handle_option_command( - {'prefix': 'dashboard get-grafana-api-port'}) + {'prefix': 'dashboard get-grafana-api-port'}, + None + ) self.assertEqual(r, 0) self.assertEqual(out, '5000') self.assertEqual(err, '') r, out, err = handle_option_command( {'prefix': 'dashboard set-grafana-api-host', - 'value': 'new-local-host'}) + 'value': 'new-local-host'}, + None + ) self.assertEqual(r, 0) self.assertEqual(out, 'Option GRAFANA_API_HOST updated') self.assertEqual(err, '') diff --git a/src/pybind/mgr/mgr_module.py b/src/pybind/mgr/mgr_module.py index ac4fb49df0751..e640f1a6f3626 100644 --- a/src/pybind/mgr/mgr_module.py +++ b/src/pybind/mgr/mgr_module.py @@ -5,6 +5,7 @@ try: except ImportError: # just for type checking pass +import errno import logging import json import six @@ -14,6 +15,8 @@ import rados import re import time +ERROR_MSG_EMPTY_INPUT_FILE = 'Empty content: please add a password/secret to the file.' +ERROR_MSG_NO_INPUT_FILE = 'Please specify the file containing the password/secret with "-i" option.' # Full list of strings in "osd_types.cc:pg_state_string()" PG_STATES = [ "active", @@ -165,6 +168,9 @@ class HandleCommandResult(namedtuple('HandleCommandResult', ['retval', 'stdout', return super(HandleCommandResult, cls).__new__(cls, retval, stdout, stderr) +class MonCommandFailed(RuntimeError): pass + + class OSDMap(ceph_module.BasePyOSDMap): def get_epoch(self): return self._get_epoch() @@ -388,6 +394,17 @@ def CLIWriteCommand(prefix, args="", desc=""): return CLICommand(prefix, args, desc, "w") +def CLICheckNonemptyFileInput(func): + def check(*args, **kwargs): + if not 'inbuf' in kwargs: + return -errno.EINVAL, '', ERROR_MSG_NO_INPUT_FILE + if not kwargs['inbuf'] or (isinstance(kwargs['inbuf'], str) + and not kwargs['inbuf'].strip('\n')): + return -errno.EINVAL, '', ERROR_MSG_EMPTY_INPUT_FILE + return func(*args, **kwargs) + return check + + def _get_localized_key(prefix, key): return '{}/{}'.format(prefix, key) @@ -908,7 +925,20 @@ class MgrModule(ceph_module.BaseMgrModule): """ return self._ceph_get_daemon_status(svc_type, svc_id) - def mon_command(self, cmd_dict): + def check_mon_command(self, cmd_dict, inbuf=None): + """ + Wrapper around :func:`~mgr_module.MgrModule.mon_command`, but raises, + if ``retval != 0``. + """ + + r = HandleCommandResult(*self.mon_command(cmd_dict, inbuf)) + if r.retval: + raise MonCommandFailed( + '{} failed: {} retval: {}'.format(cmd_dict["prefix"], r.stderr, r.retval) + ) + return r + + def mon_command(self, cmd_dict, inbuf=None): """ Helper for modules that do simple, synchronous mon command execution. @@ -920,7 +950,7 @@ class MgrModule(ceph_module.BaseMgrModule): t1 = time.time() result = CommandResult() - self.send_command(result, "mon", "", json.dumps(cmd_dict), "") + self.send_command(result, "mon", "", json.dumps(cmd_dict), "", inbuf) r = result.wait() t2 = time.time() @@ -930,7 +960,14 @@ class MgrModule(ceph_module.BaseMgrModule): return r - def send_command(self, *args, **kwargs): + def send_command( + self, + result, + svc_type, + svc_id, + command, + tag, + inbuf=None): """ Called by the plugin to send a command to the mon cluster. @@ -950,8 +987,9 @@ class MgrModule(ceph_module.BaseMgrModule): completes, the ``notify()`` callback on the MgrModule instance is triggered, with notify_type set to "command", and notify_id set to the tag of the command. + :param str inbuf: input buffer for sending additional data. """ - self._ceph_send_command(*args, **kwargs) + self._ceph_send_command(result, svc_type, svc_id, command, tag, inbuf) def set_health_checks(self, checks): """ diff --git a/src/test/mgr/mgr-dashboard-smoke.sh b/src/test/mgr/mgr-dashboard-smoke.sh index 582909a6ed545..99dfb26b5e3a8 100755 --- a/src/test/mgr/mgr-dashboard-smoke.sh +++ b/src/test/mgr/mgr-dashboard-smoke.sh @@ -51,7 +51,10 @@ function TEST_dashboard() { tries=$((tries+1)) sleep 1 done - ceph_adm tell mgr dashboard set-login-credentials admin admin + + DASHBOARD_ADMIN_SECRET_FILE="/tmp/dashboard-admin-secret.txt" + printf 'admin' > "${DASHBOARD_ADMIN_SECRET_FILE}" + ceph_adm dashboard ac-user-create admin -i "${DASHBOARD_ADMIN_SECRET_FILE}" tries=0 while [[ $tries < 30 ]] ; do diff --git a/src/vstart.sh b/src/vstart.sh index 9507ebf1b9d4e..f7dabd74c0b46 100755 --- a/src/vstart.sh +++ b/src/vstart.sh @@ -40,7 +40,7 @@ if [ -e CMakeCache.txt ]; then fi fi -# use CEPH_BUILD_ROOT to vstart from a 'make install' +# use CEPH_BUILD_ROOT to vstart from a 'make install' if [ -n "$CEPH_BUILD_ROOT" ]; then [ -z "$CEPH_BIN" ] && CEPH_BIN=$CEPH_BUILD_ROOT/bin [ -z "$CEPH_LIB" ] && CEPH_LIB=$CEPH_BUILD_ROOT/lib @@ -67,7 +67,7 @@ export PYTHONPATH=$PYBIND:$CEPH_LIB/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}: export LD_LIBRARY_PATH=$CEPH_LIB:$LD_LIBRARY_PATH export DYLD_LIBRARY_PATH=$CEPH_LIB:$DYLD_LIBRARY_PATH # Suppress logging for regular use that indicated that we are using a -# development version. vstart.sh is only used during testing and +# development version. vstart.sh is only used during testing and # development export CEPH_DEV=1 @@ -307,12 +307,12 @@ case $1 in -X ) cephx=0 ;; - + -g | --gssapi) - gssapi_authx=1 + gssapi_authx=1 ;; -G) - gssapi_authx=0 + gssapi_authx=0 ;; -k ) @@ -539,7 +539,7 @@ EOF auth client required = gss gss ktab client file = $CEPH_DEV_DIR/gss_\$name.keytab EOF - else + else wconf < "${DASHBOARD_ADMIN_SECRET_FILE}" + ceph_adm dashboard ac-user-create admin -i "${DASHBOARD_ADMIN_SECRET_FILE}" \ + administrator if [ "$ssl" != "0" ]; then if ! ceph_adm tell mgr dashboard create-self-signed-cert; then echo dashboard module not working correctly! -- 2.39.5