]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/cephadm/cephadm
import ceph 15.2.14
[ceph.git] / ceph / src / cephadm / cephadm
index 8de809d75bf126dd0aef5a65040906b088a0fd6a..9b7348b8bc29c5881c05511b478ebcca728818a5 100755 (executable)
@@ -10,6 +10,7 @@ LOGROTATE_DIR = '/etc/logrotate.d'
 UNIT_DIR = '/etc/systemd/system'
 LOG_DIR_MODE = 0o770
 DATA_DIR_MODE = 0o700
+CONTAINER_INIT=False
 CONTAINER_PREFERENCE = ['podman', 'docker']  # prefer podman to docker
 CUSTOM_PS1 = r'[ceph: \u@\h \W]\$ '
 DEFAULT_TIMEOUT = None  # in seconds
@@ -49,6 +50,7 @@ import platform
 import pwd
 import random
 import select
+import shlex
 import shutil
 import socket
 import string
@@ -182,7 +184,7 @@ class Monitoring(object):
             ],
         },
         "grafana": {
-            "image": "docker.io/ceph/ceph-grafana:6.6.2",
+            "image": "docker.io/ceph/ceph-grafana:6.7.4",
             "cpus": "2",
             "memory": "4GB",
             "args": [],
@@ -210,6 +212,35 @@ class Monitoring(object):
         },
     }  # type: ignore
 
+    @staticmethod
+    def get_version(container_path, container_id, daemon_type):
+        # type: (str, str, str) -> str
+        """
+        :param: daemon_type Either "prometheus", "alertmanager" or "node-exporter"
+        """
+        assert daemon_type in ('prometheus', 'alertmanager', 'node-exporter')
+        cmd = daemon_type.replace('-', '_')
+        code = -1
+        err = ''
+        version = ''
+        if daemon_type == 'alertmanager':
+            for cmd in ['alertmanager', 'prometheus-alertmanager']:
+                _, err, code = call([
+                        container_path, 'exec', container_id, cmd,
+                        '--version'
+                    ], verbosity=CallVerbosity.SILENT)
+                if code == 0:
+                    break
+            cmd = 'alertmanager'  # reset cmd for version extraction
+        else:
+            _, err, code = call([
+                container_path, 'exec', container_id, cmd, '--version'
+            ])
+        if code == 0 and \
+            err.startswith('%s, version ' % cmd):
+            version = err.split(' ')[2]
+        return version
+
 ##################################
 
 
@@ -682,6 +713,9 @@ def get_supported_daemons():
 
 ##################################
 
+class PortOccupiedError(Error):
+    pass
+
 
 def attempt_bind(s, address, port):
     # type: (socket.socket, str, int) -> None
@@ -689,12 +723,12 @@ def attempt_bind(s, address, port):
         s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         s.bind((address, port))
     except (socket.error, OSError) as e:  # py2 and py3
-        msg = 'Cannot bind to IP %s port %d: %s' % (address, port, e)
-        logger.warning(msg)
         if e.errno == errno.EADDRINUSE:
-            raise OSError(msg)
-        elif e.errno == errno.EADDRNOTAVAIL:
-            pass
+            msg = 'Cannot bind to IP %s port %d: %s' % (address, port, e)
+            logger.warning(msg)
+            raise PortOccupiedError(msg)
+        else:
+            raise e
     finally:
         s.close()
 
@@ -703,16 +737,26 @@ def port_in_use(port_num):
     # type: (int) -> bool
     """Detect whether a port is in use on the local machine - IPv4 and IPv6"""
     logger.info('Verifying port %d ...' % port_num)
-    try:
-        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        attempt_bind(s, '0.0.0.0', port_num)
-
-        s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
-        attempt_bind(s, '::', port_num)
-    except OSError:
-        return True
-    else:
+    def _port_in_use(af, address):
+        # type: (socket.AddressFamily, str) -> bool
+        try:
+            s = socket.socket(af, socket.SOCK_STREAM)
+            attempt_bind(s, address, port_num)
+        except PortOccupiedError:
+            return True
+        except OSError as e:
+            if e.errno in (errno.EAFNOSUPPORT, errno.EADDRNOTAVAIL):
+                # Ignore EAFNOSUPPORT and EADDRNOTAVAIL as two interfaces are
+                # being tested here and one might be intentionally be disabled.
+                # In that case no error should be raised.
+                return False
+            else:
+                raise e
         return False
+    return any(_port_in_use(af, address) for af, address in (
+        (socket.AF_INET, '0.0.0.0'),
+        (socket.AF_INET6, '::')
+    ))
 
 
 def check_ip_port(ip, port):
@@ -724,10 +768,7 @@ def check_ip_port(ip, port):
             ip = unwrap_ipv6(ip)
         else:
             s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        try:
-            attempt_bind(s, ip, port)
-        except OSError as e:
-            raise Error(e)
+        attempt_bind(s, ip, port)
 
 ##################################
 
@@ -1685,6 +1726,13 @@ def check_units(units, enabler=None):
     return False
 
 
+def is_container_running(name: str) -> bool:
+    out, err, ret = call_throws([
+        container_path, 'ps',
+        '--format', '{{.Names}}'])
+    return name in out
+
+
 def get_legacy_config_fsid(cluster, legacy_dir=None):
     # type: (str, Optional[str]) -> Optional[str]
     config_file = '/etc/ceph/%s.conf' % cluster
@@ -1728,7 +1776,7 @@ def get_daemon_args(fsid, daemon_type, daemon_id):
             '--setgroup', 'ceph',
             '--default-log-to-file=false',
             '--default-log-to-stderr=true',
-            '--default-log-stderr-prefix="debug "',
+            '--default-log-stderr-prefix=debug ',
         ]
         if daemon_type == 'mon':
             r += [
@@ -1777,7 +1825,6 @@ def create_daemon_dirs(fsid, daemon_type, daemon_id, uid, gid,
 
     if daemon_type in Monitoring.components.keys():
         config_json: Dict[str, Any] = get_parm(args.config_json)
-        required_files = Monitoring.components[daemon_type].get('config-json-files', list())
 
         # Set up directories specific to the monitoring component
         config_dir = ''
@@ -1801,10 +1848,14 @@ def create_daemon_dirs(fsid, daemon_type, daemon_id, uid, gid,
             makedirs(os.path.join(data_dir_root, config_dir, 'data'), uid, gid, 0o755)
 
         # populate the config directory for the component from the config-json
-        for fname in required_files:
-            if 'files' in config_json:  # type: ignore
+        if 'files' in config_json:
+            for fname in config_json['files']:
                 content = dict_get_join(config_json['files'], fname)
-                with open(os.path.join(data_dir_root, config_dir, fname), 'w') as f:
+                if os.path.isabs(fname):
+                    fpath = os.path.join(data_dir_root, fname.lstrip(os.path.sep))
+                else:
+                    fpath = os.path.join(data_dir_root, config_dir, fname)
+                with open(fpath, 'w', encoding='utf-8') as f:
                     os.fchown(f.fileno(), uid, gid)
                     os.fchmod(f.fileno(), 0o600)
                     f.write(content)
@@ -2067,7 +2118,6 @@ def get_container(fsid: str, daemon_type: str, daemon_id: Union[int, str],
         envs=envs,
         privileged=privileged,
         ptrace=ptrace,
-        init=args.container_init,
         host_network=host_network,
     )
 
@@ -2194,10 +2244,15 @@ def _write_container_cmd_to_bash(file_obj, container, comment=None, background=F
     file_obj.write('! '+ ' '.join(container.rm_cmd()) + ' 2> /dev/null\n')
     # Sometimes, `podman rm` doesn't find the container. Then you'll have to add `--storage`
     if 'podman' in container_path:
-        file_obj.write('! '+ ' '.join(container.rm_cmd(storage=True)) + ' 2> /dev/null\n')
+        file_obj.write(
+            '! '
+            + ' '.join([shlex.quote(a) for a in container.rm_cmd(storage=True)])
+            + ' 2> /dev/null\n')
 
     # container run command
-    file_obj.write(' '.join(container.run_cmd()) + (' &' if background else '') + '\n')
+    file_obj.write(
+        ' '.join([shlex.quote(a) for a in container.run_cmd()])
+        + (' &' if background else '') + '\n')
 
 
 def deploy_daemon_units(fsid, uid, gid, daemon_type, daemon_id, c,
@@ -2381,7 +2436,6 @@ class Firewalld(object):
             else:
                 logger.debug('firewalld port %s is enabled in current zone' % tcp_port)
 
-            out, err, ret = call([self.cmd, '--permanent', '--query-port', tcp_port], verbose_on_failure=False)
     def apply_rules(self):
         # type: () -> None
         if not self.available:
@@ -2482,6 +2536,7 @@ def get_unit_file(fsid):
             'Type=forking\n'
             'PIDFile=/%t/%n-pid\n')
 
+    docker = 'docker' in container_path
     u = """# generated by cephadm
 [Unit]
 Description=Ceph %i for {fsid}
@@ -2490,8 +2545,9 @@ Description=Ceph %i for {fsid}
 #   http://www.freedesktop.org/wiki/Software/systemd/NetworkTarget
 # these can be removed once ceph-mon will dynamically change network
 # configuration.
-After=network-online.target local-fs.target time-sync.target
+After=network-online.target local-fs.target time-sync.target{docker_after}
 Wants=network-online.target local-fs.target time-sync.target
+{docker_requires}
 
 PartOf=ceph-{fsid}.target
 Before=ceph-{fsid}.target
@@ -2517,7 +2573,11 @@ WantedBy=ceph-{fsid}.target
     container_path=container_path,
     fsid=fsid,
     data_dir=args.data_dir,
-    extra_args=extra_args)
+    extra_args=extra_args,
+    # if docker, we depend on docker.service
+    docker_after=' docker.service' if docker else '',
+    docker_requires='Requires=docker.service\n' if docker else '',
+)
 
     return u
 
@@ -2536,7 +2596,7 @@ class CephContainer:
                  privileged: bool = False,
                  ptrace: bool = False,
                  bind_mounts: Optional[List[List[str]]] = None,
-                 init: bool = False,
+                 init: Optional[bool] = None,
                  host_network: bool = True,
                  ) -> None:
         self.image = image
@@ -2549,7 +2609,7 @@ class CephContainer:
         self.privileged = privileged
         self.ptrace = ptrace
         self.bind_mounts = bind_mounts if bind_mounts else []
-        self.init = init
+        self.init = init if init else container_init
         self.host_network = host_network
 
     def run_cmd(self) -> List[str]:
@@ -2582,6 +2642,7 @@ class CephContainer:
             cmd_args.append('--cap-add=SYS_PTRACE')
         if self.init:
             cmd_args.append('--init')
+            envs += ['-e', 'CEPH_USE_RANDOM_NONCE=1']
         if self.cname:
             cmd_args.extend(['--name', self.cname])
         if self.envs:
@@ -2620,6 +2681,9 @@ class CephContainer:
                 # let OSD etc read block devs that haven't been chowned
                 '--group-add=disk',
             ])
+        if self.init:
+            cmd_args.append('--init')
+            envs += ['-e', 'CEPH_USE_RANDOM_NONCE=1']
         if self.envs:
             for env in self.envs:
                 envs.extend(['-e', env])
@@ -2723,7 +2787,7 @@ def command_inspect_image():
     # type: () -> int
     out, err, ret = call_throws([
         container_path, 'inspect',
-        '--format', '{{.ID}},{{json .RepoDigests}}',
+        '--format', '{{.ID}},{{.RepoDigests}}',
         args.image])
     if ret:
         return errno.ENOENT
@@ -2745,7 +2809,7 @@ def get_image_info_from_inspect(out, image):
         'image_id': normalize_container_id(image_id)
     }
     if digests:
-        json_digests = json.loads(digests)
+        json_digests = digests[1:-1].split(' ')
         if json_digests:
             r['repo_digest'] = json_digests[0]
     return r
@@ -2898,8 +2962,15 @@ def command_bootstrap():
     if not cp.has_section('global'):
         cp.add_section('global')
     cp.set('global', 'fsid', fsid);
-    cp.set('global', 'mon host', addr_arg)
+    cp.set('global', 'mon_host', addr_arg)
     cp.set('global', 'container_image', args.image)
+    if not cp.has_section('mon'):
+        cp.add_section('mon')
+    if (
+            not cp.has_option('mon', 'auth_allow_insecure_global_id_reclaim')
+            and not cp.has_option('mon', 'auth allow insecure global id reclaim')
+    ):
+        cp.set('mon', 'auth_allow_insecure_global_id_reclaim', 'false')
     cpf = StringIO()
     cp.write(cpf)
     config = cpf.getvalue()
@@ -3225,8 +3296,7 @@ def command_bootstrap():
         cli(['config', 'set', 'mgr', 'mgr/cephadm/registry_username', args.registry_username, '--force'])
         cli(['config', 'set', 'mgr', 'mgr/cephadm/registry_password', args.registry_password, '--force'])
 
-    if args.container_init:
-        cli(['config', 'set', 'mgr', 'mgr/cephadm/container_init', str(args.container_init), '--force'])
+    cli(['config', 'set', 'mgr', 'mgr/cephadm/container_init', str(container_init), '--force'])
 
     if not args.skip_dashboard:
         # Configure SSL port (cephadm only allows to configure dashboard SSL port)
@@ -3253,10 +3323,11 @@ def command_bootstrap():
 
         logger.info('Creating initial admin user...')
         password = args.initial_dashboard_password or generate_password()
-        cmd = ['dashboard', 'ac-user-create', args.initial_dashboard_user, password, 'administrator', '--force-password']
+        tmp_password_file = write_tmp(password, uid, gid)
+        cmd = ['dashboard', 'ac-user-create', args.initial_dashboard_user, '-i', '/tmp/dashboard.pw', 'administrator', '--force-password']
         if not args.dashboard_password_noupdate:
             cmd.append('--pwd-update-required')
-        cli(cmd)
+        cli(cmd, extra_mounts={pathify(tmp_password_file.name): '/tmp/dashboard.pw:z'})
         logger.info('Fetching dashboard port number...')
         out = cli(['config', 'get', 'mgr', 'mgr/dashboard/ssl_server_port'])
         port = int(out)
@@ -3288,7 +3359,7 @@ def command_bootstrap():
                         ssh_key = '/etc/ceph/ceph.pub'
                         if args.ssh_public_key:
                             ssh_key = args.ssh_public_key.name
-                        out, err, code = call_throws(['ssh-copy-id', '-f', '-i', ssh_key, '%s@%s' % (args.ssh_user, split[1])])
+                        out, err, code = call_throws(['sudo', '-u', args.ssh_user, 'ssh-copy-id', '-f', '-i', ssh_key, '-o StrictHostKeyChecking=no', '%s@%s' % (args.ssh_user, split[1])])
 
         mounts = {}
         mounts[pathify(args.apply_spec)] = '/tmp/spec.yml:z'
@@ -3378,8 +3449,9 @@ def command_deploy():
 
     redeploy = False
     unit_name = get_unit_name(args.fsid, daemon_type, daemon_id)
+    container_name = 'ceph-%s-%s.%s' % (args.fsid, daemon_type, daemon_id)
     (_, state, _) = check_unit(unit_name)
-    if state == 'running':
+    if state == 'running' or is_container_running(container_name):
         redeploy = True
 
     if args.reconfig:
@@ -3870,14 +3942,8 @@ def list_daemons(detail=True, legacy_dir=None):
                                 elif daemon_type in ['prometheus',
                                                      'alertmanager',
                                                      'node-exporter']:
-                                    cmd = daemon_type.replace('-', '_')
-                                    out, err, code = call(
-                                        [container_path, 'exec', container_id,
-                                         cmd, '--version'])
-                                    if not code and \
-                                       err.startswith('%s, version ' % cmd):
-                                        version = err.split(' ')[2]
-                                        seen_versions[image_id] = version
+                                    version = Monitoring.get_version(container_path, container_id, daemon_type)
+                                    seen_versions[image_id] = version
                                 elif daemon_type == CustomContainer.daemon_type:
                                     # Because a custom container can contain
                                     # everything, we do not know which command
@@ -5610,6 +5676,11 @@ def _get_parser():
         action='append',
         default=[],
         help='set environment variable')
+    parser.add_argument(
+        '--no-container-init',
+        action='store_true',
+        default=not CONTAINER_INIT,
+        help='Do not run podman/docker with `--init`')
 
     subparsers = parser.add_subparsers(help='sub-command')
 
@@ -5678,7 +5749,8 @@ def _get_parser():
     parser_adopt.add_argument(
         '--container-init',
         action='store_true',
-        help='Run podman/docker with `--init`')
+        default=CONTAINER_INIT,
+        help=argparse.SUPPRESS)
 
     parser_rm_daemon = subparsers.add_parser(
         'rm-daemon', help='remove daemon instance')
@@ -5967,7 +6039,8 @@ def _get_parser():
     parser_bootstrap.add_argument(
         '--container-init',
         action='store_true',
-        help='Run podman/docker with `--init`')
+        default=CONTAINER_INIT,
+        help=argparse.SUPPRESS)
 
     parser_deploy = subparsers.add_parser(
         'deploy', help='deploy a daemon')
@@ -6014,7 +6087,8 @@ def _get_parser():
     parser_deploy.add_argument(
         '--container-init',
         action='store_true',
-        help='Run podman/docker with `--init`')
+        default=CONTAINER_INIT,
+        help=argparse.SUPPRESS)
 
     parser_check_host = subparsers.add_parser(
         'check-host', help='check host configuration')
@@ -6094,9 +6168,22 @@ def _get_parser():
 
 def _parse_args(av):
     parser = _get_parser()
+
     args = parser.parse_args(av)
     if 'command' in args and args.command and args.command[0] == "--":
         args.command.pop(0)
+
+    # workaround argparse to deprecate the subparser `--container-init` flag
+    # container_init and no_container_init must always be mutually exclusive
+    container_init_args = ('--container-init', '--no-container-init')
+    if set(container_init_args).issubset(av):
+        parser.error('argument %s: not allowed with argument %s' % (container_init_args))
+    elif '--container-init' in av:
+        args.no_container_init = not args.container_init
+    else:
+        args.container_init = not args.no_container_init
+    assert args.container_init is not args.no_container_init
+
     return args
 
 
@@ -6147,6 +6234,10 @@ if __name__ == "__main__":
                 sys.stderr.write('Unable to locate any of %s\n' % CONTAINER_PREFERENCE)
                 sys.exit(1)
 
+    # container-init?
+    container_init = args.container_init
+    logger.debug('container_init=%s' % (container_init))
+
     try:
         r = args.func()
     except Error as e: