]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: CLI commands: read passwords from file
authorAlfonso Martínez <almartin@redhat.com>
Tue, 15 Dec 2020 08:28:46 +0000 (09:28 +0100)
committerAlfonso Martínez <almartin@redhat.com>
Fri, 8 Jan 2021 18:36:03 +0000 (19:36 +0100)
Fixes: https://tracker.ceph.com/issues/48355
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
Signed-off-by: Juan Miguel Olmo Martínez <jolmomar@redhat.com>
(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.

17 files changed:
doc/mgr/dashboard.rst
qa/tasks/mgr/dashboard/helper.py
qa/tasks/mgr/dashboard/test_auth.py
qa/tasks/mgr/dashboard/test_ganesha.py
qa/tasks/mgr/dashboard/test_rgw.py
src/pybind/mgr/dashboard/module.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/settings.py
src/pybind/mgr/dashboard/tests/__init__.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/test/mgr/mgr-dashboard-smoke.sh
src/vstart.sh

index 206a04787455da6b39652cbe8acf9a295bbfc3bf..5d14be6c91cf8231c13b3e68808baf707afc2f1a 100644 (file)
@@ -256,7 +256,7 @@ section.
 To create a user with the administrator role you can use the following
 commands::
 
-  $ ceph dashboard ac-user-create <username> <password> administrator
+  $ ceph dashboard ac-user-create <username> -i <file-containing-password> 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 <access_key>
-  $ ceph dashboard set-rgw-api-secret-key <secret_key>
+  $ ceph dashboard set-rgw-api-access-key -i <file-containing-access-key>
+  $ ceph dashboard set-rgw-api-secret-key -i <file-containing-secret-key>
 
 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 <scheme>://<username>:<password>@<host>[:port]
-    $ ceph dashboard iscsi-gateway-rm <gateway_name>
+  $ ceph dashboard iscsi-gateway-list
+  $ # Gateway URL format for a new gateway: <scheme>://<username>:<password>@<host>[:port]
+  $ ceph dashboard iscsi-gateway-add -i <file-containing-gateway-url> [<gateway_name>]
+  $ ceph dashboard iscsi-gateway-rm <gateway_name>
 
 
 .. _dashboard-grafana:
@@ -646,7 +647,7 @@ We provide a set of CLI commands to manage user accounts:
 
 - *Create User*::
 
-  $ ceph dashboard ac-user-create <username> [<password>] [<rolename>] [<name>] [<email>]
+  $ ceph dashboard ac-user-create <username> -i <file-containing-password> [<rolename>] [<name>] [<email>]
 
 - *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 <username> <password>
+  $ ceph dashboard ac-user-set-password <username> -i <file-containing-password>
 
 - *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 <file-containing-password>
 
 2. *Create role and specify scope permissions*::
 
index 9627a8428f84e8115e5da6c97c549148135bff34..d7e6accee0b8162af4d3f0f1e235f78fbb744516 100644 (file)
@@ -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):
index 0acc64478d17d81eea5ac28974c5f1a8471fefa6..6603b85928ba243938acd9f825acee57e797c81b 100644 (file)
@@ -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)
index cd869a00e405b2f251fc88f81e4f98fd74e040c2..b90bb4afcfeb8e531956b8e75e5ea720d2204cd8 100644 (file)
@@ -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):
index dadec1091634e019570756bbf511188f81426caa..64c185291d3dc366027f6b375e3eb68dbed8ddef 100644 (file)
@@ -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)
index ab78c8c697b2ff0518b3ee58aa43863e3d3e7de2..ee34e49cc344ea5f23ce83c0ae8c1b5867dd6210 100644 (file)
@@ -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)
index 87a1e6b4697b4a9212d931f548ff60177a3b09ec..25e2963450e542f766c1c3b069069183d0efe113 100755 (executable)
@@ -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
 
index 3787be62d482d07898e67cde4b3b26eb0b5da193..16a31285f49b5a90f2d019d859246d2595fabebc 100644 (file)
@@ -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 <file>')
+@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 <file>')
+@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 <file>')
+@CLICheckNonemptyFileInput
+def ac_user_set_password(_, username, inbuf):
+    password = inbuf
     try:
         user = mgr.ACCESS_CTRL_DB.get_user(username)
         user.set_password(password)
index fca1f61b3cc14db1a24a176f935bad97ba016bfb..900c79af43d0ebb06473104e84a15a67b5a70f1a 100644 (file)
@@ -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 <file>')
+@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']
index 31e09fac5ed215d2a66b8d37131d173126809711..d9936b5008d4ce8bb8914d31c386c72bb144216c 100644 (file)
@@ -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 <file>'.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
index 449b4245ec8b51087a44cb3449fbb38ef7f8797f..589986903b705486cba51d1ba4c34e6c283106c9 100644 (file)
@@ -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:
index 68bdaf1aa79f70b600b9f8023d9287d3b3f951b6..268a4f1861e9b6c8db5d8d376f652d86fc30dde2 100644 (file)
@@ -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, {
index 4a6540f52def06103e8c481a309ff92bff916ad2..34d2f014a9c11d5fa30aa1cbf7088fc3118819b4 100644 (file)
@@ -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: "
                          "'<scheme>://<username>:<password>@<host>[: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': {
index aac785b7ce03b057878311da45d151a5e3b48713..2d0504297c78b0ed515c0986ab21129750a26cb4 100644 (file)
@@ -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, '')
index ac4fb49df07512ce8e58ddfe597eae3317d53595..e640f1a6f3626d24cb2d4141c5106c3e8d551fd4 100644 (file)
@@ -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):
         """
index 582909a6ed545067b9c30a66706c9f429d07e499..99dfb26b5e3a8a7f57f3b2926b2c68d171fa6da5 100755 (executable)
@@ -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
index 9507ebf1b9d4e13b7a3be7de94807f4d7ee5dac3..f7dabd74c0b462f2ebcfebb70121e72d0ce60cd6 100755 (executable)
@@ -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 <<EOF
        auth cluster required = none
        auth service required = none
@@ -828,7 +828,10 @@ EOF
     if [ "$new" -eq 1 ]; then
         # setting login credentials for dashboard
         if $with_mgr_dashboard; then
-            ceph_adm tell mgr dashboard ac-user-create admin admin administrator
+            DASHBOARD_ADMIN_SECRET_FILE="${CEPH_CONF_PATH}/dashboard-admin-secret.txt"
+            printf 'admin' > "${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!