]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
files,rpm,deb: rename ceph-daemon -> cephadm
authorSage Weil <sage@redhat.com>
Wed, 11 Dec 2019 19:48:02 +0000 (13:48 -0600)
committerSage Weil <sage@redhat.com>
Thu, 12 Dec 2019 01:14:09 +0000 (19:14 -0600)
This is just renaming the files and adjusting the packages.  Lots of
cleanup to do still.

Signed-off-by: Sage Weil <sage@redhat.com>
49 files changed:
.github/CODEOWNERS
ceph.spec.in
debian/ceph-daemon.install [deleted file]
debian/ceph-daemon.postinst [deleted file]
debian/ceph-daemon.postrm [deleted file]
debian/cephadm.install [new file with mode: 0644]
debian/cephadm.postinst [new file with mode: 0644]
debian/cephadm.postrm [new file with mode: 0644]
debian/control
debian/rules
qa/packages/packages.yaml
qa/standalone/test_ceph_daemon.sh
qa/suites/fs/upgrade/featureful_client/old_client/tasks/0-mimic.yaml
qa/suites/fs/upgrade/featureful_client/upgraded_client/tasks/0-mimic.yaml
qa/suites/rados/ssh/mode/packaged.yaml
qa/suites/rados/thrash-old-clients/1-install/hammer.yaml
qa/suites/rados/thrash-old-clients/1-install/jewel-v1only.yaml
qa/suites/rados/thrash-old-clients/1-install/jewel.yaml
qa/suites/rados/thrash-old-clients/1-install/luminous-v1only.yaml
qa/suites/rados/thrash-old-clients/1-install/luminous.yaml
qa/suites/rados/thrash-old-clients/1-install/mimic-v1only.yaml
qa/suites/rados/thrash-old-clients/1-install/mimic.yaml
qa/suites/rados/thrash-old-clients/1-install/nautilus-v1only.yaml
qa/suites/rados/thrash-old-clients/1-install/nautilus-v2only.yaml
qa/suites/rados/thrash-old-clients/1-install/nautilus.yaml
qa/suites/upgrade/mimic-x-singleton/1-install/mimic.yaml
qa/suites/upgrade/mimic-x/parallel/1-ceph-install/mimic.yaml
qa/suites/upgrade/mimic-x/stress-split/1-ceph-install/mimic.yaml
qa/suites/upgrade/nautilus-x-singleton/1-install/nautilus.yaml
qa/tasks/ceph2.py
src/CMakeLists.txt
src/ceph-daemon/CMakeLists.txt [deleted file]
src/ceph-daemon/ceph-daemon [deleted file]
src/ceph-daemon/mypy.ini [deleted file]
src/ceph-daemon/tests/__init__.py [deleted file]
src/ceph-daemon/tests/test_ceph_daemon.py [deleted file]
src/ceph-daemon/tox.ini [deleted file]
src/cephadm/CMakeLists.txt [new file with mode: 0644]
src/cephadm/cephadm [new file with mode: 0755]
src/cephadm/mypy.ini [new file with mode: 0644]
src/cephadm/tests/__init__.py [new file with mode: 0644]
src/cephadm/tests/test_ceph_daemon.py [new file with mode: 0644]
src/cephadm/tox.ini [new file with mode: 0644]
src/common/options.cc
src/pybind/mgr/ssh/module.py
src/vstart.sh
sudoers.d/cephadm [new file with mode: 0644]
sudoers.d/cephdaemon [deleted file]
test_ceph_daemon.sh

index 685382b97ba18430e3215fd61652330b28f46522..51237b2330a1f761941b07553909cc2e29ab59b9 100644 (file)
@@ -8,7 +8,7 @@
 /doc/mgr/dashboard.rst                          @ceph/dashboard
 
 # For Orchestrator related PRs
-/src/ceph-daemon                                @ceph/orchestrators
+/src/cephadm                                    @ceph/orchestrators
 /src/pybind/mgr/ansible                         @ceph/orchestrators
 /src/pybind/mgr/orchestrator_cli                @ceph/orchestrators
 /src/pybind/mgr/orchestrator.py                 @ceph/orchestrators
index 1adb2d7710861dc32668585a695304af2f70eb1e..968f25dc5375c64546b660120495c87c5604146b 100644 (file)
@@ -400,11 +400,11 @@ Recommends:    chrony
 %description base
 Base is the package that includes all the files shared amongst ceph servers
 
-%package -n ceph-daemon
-Summary:        Ceph-daemon utility to bootstrap Ceph clusters
+%package -n cephadm
+Summary:        cephadm utility to bootstrap Ceph clusters
 Requires:       podman
-%description -n ceph-daemon
-Ceph-daemon utility to bootstrap a Ceph cluster and manage ceph daemons
+%description -n cephadm
+cephadm utility to bootstrap a Ceph cluster and manage ceph daemons
 deployed with systemd and podman.
 
 %package -n ceph-common
@@ -604,7 +604,7 @@ Group:          System/Filesystems
 %endif
 Requires:       ceph-mgr = %{_epoch_prefix}%{version}-%{release}
 Requires:       python%{_python_buildid}-remoto
-Requires:       ceph-daemon = %{_epoch_prefix}%{version}-%{release}
+Requires:       cephadm = %{_epoch_prefix}%{version}-%{release}
 %if 0%{?suse_version}
 Requires:       openssh
 %endif
@@ -1349,7 +1349,7 @@ chmod 0644 %{buildroot}%{_docdir}/ceph/sample.ceph.conf
 install -m 0644 -D COPYING %{buildroot}%{_docdir}/ceph/COPYING
 install -m 0644 -D etc/sysctl/90-ceph-osd.conf %{buildroot}%{_sysctldir}/90-ceph-osd.conf
 
-install -m 0755 src/ceph-daemon/ceph-daemon %{buildroot}%{_sbindir}/ceph-daemon
+install -m 0755 src/cephadm/cephadm %{buildroot}%{_sbindir}/cephadm
 
 # firewall templates and /sbin/mount.ceph symlink
 %if 0%{?suse_version}
@@ -1362,7 +1362,7 @@ install -m 0644 -D udev/50-rbd.rules %{buildroot}%{_udevrulesdir}/50-rbd.rules
 
 # sudoers.d
 install -m 0600 -D sudoers.d/ceph-osd-smartctl %{buildroot}%{_sysconfdir}/sudoers.d/ceph-osd-smartctl
-install -m 0600 -D sudoers.d/cephdaemon %{buildroot}%{_sysconfdir}/sudoers.d/cephdaemon
+install -m 0600 -D sudoers.d/cephadm %{buildroot}%{_sysconfdir}/sudoers.d/cephadm
 
 %if 0%{?rhel} >= 8
 pathfix.py -pni "%{__python3} %{py3_shbang_opts}" %{buildroot}%{_bindir}/*
@@ -1510,31 +1510,31 @@ if [ $1 -ge 1 ] ; then
   fi
 fi
 
-%pre daemon
+%pre -n cephadm
 # create user
-if ! getent passwd | grep -q '^cephdaemon:'; then
-   useradd -r -s /bin/bash -c "Ceph-daemon user for mgr/ssh" -m cephdaemon
+if ! getent passwd | grep -q '^cephadm:'; then
+   useradd -r -s /bin/bash -c "cephadm user for mgr/ssh" -m cephadm
 fi
 # set up (initially empty) .ssh/authorized_keys file
-if ! test -d /home/cephdaemon/.ssh; then
-   mkdir /home/cephdaemon/.ssh
-   chown --reference /home/cephdaemon /home/cephdaemon/.ssh
-   chmod 0700 /home/cephdaemon/.ssh
+if ! test -d /home/cephadm/.ssh; then
+   mkdir /home/cephadm/.ssh
+   chown --reference /home/cephadm /home/cephadm/.ssh
+   chmod 0700 /home/cephadm/.ssh
 fi
-if ! test -e /home/cephdaemon/.ssh/authorized_keys; then
-   touch /home/cephdaemon/.ssh/authorized_keys
-   chown --reference /home/cephdaemon /home/cephdaemon/.ssh/authorized_keys
-   chmod 0600 /home/cephdaemon/.ssh/authorized_keys
+if ! test -e /home/cephadm/.ssh/authorized_keys; then
+   touch /home/cephadm/.ssh/authorized_keys
+   chown --reference /home/cephadm /home/cephadm/.ssh/authorized_keys
+   chmod 0600 /home/cephadm/.ssh/authorized_keys
 fi
 exit 0
 
-%postun daemon
-userdel -r cephdaemon || true
+%postun -n cephadm
+userdel -r cephadm || true
 exit 0
 
-%files daemon
-%{_sbindir}/ceph-daemon
-%{_sysconfdir}/sudoers.d/cephdaemon
+%files -n cephadm
+%{_sbindir}/cephadm
+%{_sysconfdir}/sudoers.d/cephadm
 
 %files common
 %dir %{_docdir}/ceph
diff --git a/debian/ceph-daemon.install b/debian/ceph-daemon.install
deleted file mode 100644 (file)
index 014e78e..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-usr/sbin/ceph-daemon
-etc/sudoers.d/cephdaemon
diff --git a/debian/ceph-daemon.postinst b/debian/ceph-daemon.postinst
deleted file mode 100644 (file)
index d0e0565..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/bin/sh
-# vim: set noet ts=8:
-# postinst script for ceph-daemon
-#
-# see: dh_installdeb(1)
-
-set -e
-
-# summary of how this script can be called:
-#
-#      postinst configure <most-recently-configured-version>
-#      old-postinst abort-upgrade <new-version>
-#      conflictor's-postinst abort-remove in-favour <package> <new-version>
-#      postinst abort-remove
-#      deconfigured's-postinst abort-deconfigure in-favour <failed-install-package> <version> [<removing conflicting-package> <version>]
-#
-
-# for details, see http://www.debian.org/doc/debian-policy/ or
-# the debian-policy package
-
-
-case "$1" in
-    configure)
-       # create cephdaemon user
-       # 1. create user if not existing
-       if ! getent passwd | grep -q "^cephdaemon:"; then
-         echo -n "Adding system user cephdaemon.."
-         adduser --quiet --system --disabled-password --gecos 'Ceph-dameon user for mgr/ssh' --shell /bin/bash cephdaemon 2>/dev/null || true
-         echo "..done"
-       fi
-
-       # 2. make sure user is unlocked
-       if [ -f /etc/shadow ]; then
-           usermod -U -e '' cephdaemon
-       else
-           usermod -U cephdaemon
-       fi
-
-       # set up (initially empty) .ssh/authorized_keys file
-       if ! test -d /home/cephdaemon/.ssh; then
-          mkdir /home/cephdaemon/.ssh
-          chown --reference /home/cephdaemon /home/cephdaemon/.ssh
-          chmod 0700 /home/cephdaemon/.ssh
-       fi
-       if ! test -e /home/cephdaemon/.ssh/authorized_keys; then
-          touch /home/cephdaemon/.ssh/authorized_keys
-          chown --reference /home/cephdaemon /home/cephdaemon/.ssh/authorized_keys
-          chmod 0600 /home/cephdaemon/.ssh/authorized_keys
-       fi
-
-    ;;
-    abort-upgrade|abort-remove|abort-deconfigure)
-       :
-    ;;
-
-    *)
-        echo "postinst called with unknown argument \`$1'" >&2
-        exit 1
-    ;;
-esac
-
-# dh_installdeb will replace this with shell code automatically
-# generated by other debhelper scripts.
-
-#DEBHELPER#
-
-exit 0
diff --git a/debian/ceph-daemon.postrm b/debian/ceph-daemon.postrm
deleted file mode 100644 (file)
index 564ffef..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/sh
-# postrm script for ceph-daemon
-#
-# see: dh_installdeb(1)
-
-set -e
-
-# summary of how this script can be called:
-#        * <postrm> `remove'
-#        * <postrm> `purge'
-#        * <old-postrm> `upgrade' <new-version>
-#        * <new-postrm> `failed-upgrade' <old-version>
-#        * <new-postrm> `abort-install'
-#        * <new-postrm> `abort-install' <old-version>
-#        * <new-postrm> `abort-upgrade' <old-version>
-#        * <disappearer's-postrm> `disappear' <overwriter>
-#          <overwriter-version>
-# for details, see http://www.debian.org/doc/debian-policy/ or
-# the debian-policy package
-
-
-case "$1" in
-    remove)
-       deluser --remove-home cephdaemon
-    ;;
-
-    purge)
-    ;;
-
-    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
-    ;;
-
-    *)
-        echo "postrm called with unknown argument \`$1'" >&2
-        exit 1
-    ;;
-esac
-
-# dh_installdeb will replace this with shell code automatically
-# generated by other debhelper scripts.
-
-#DEBHELPER#
-
-exit 0
diff --git a/debian/cephadm.install b/debian/cephadm.install
new file mode 100644 (file)
index 0000000..262f3ee
--- /dev/null
@@ -0,0 +1,2 @@
+usr/sbin/cephadm
+etc/sudoers.d/cephadm
diff --git a/debian/cephadm.postinst b/debian/cephadm.postinst
new file mode 100644 (file)
index 0000000..4b2bdf7
--- /dev/null
@@ -0,0 +1,67 @@
+#!/bin/sh
+# vim: set noet ts=8:
+# postinst script for cephadm
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#
+#      postinst configure <most-recently-configured-version>
+#      old-postinst abort-upgrade <new-version>
+#      conflictor's-postinst abort-remove in-favour <package> <new-version>
+#      postinst abort-remove
+#      deconfigured's-postinst abort-deconfigure in-favour <failed-install-package> <version> [<removing conflicting-package> <version>]
+#
+
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+    configure)
+       # create cephadm user
+       # 1. create user if not existing
+       if ! getent passwd | grep -q "^cephadm:"; then
+         echo -n "Adding system user cephadm.."
+         adduser --quiet --system --disabled-password --gecos 'Ceph-dameon user for mgr/ssh' --shell /bin/bash cephadm 2>/dev/null || true
+         echo "..done"
+       fi
+
+       # 2. make sure user is unlocked
+       if [ -f /etc/shadow ]; then
+           usermod -U -e '' cephadm
+       else
+           usermod -U cephadm
+       fi
+
+       # set up (initially empty) .ssh/authorized_keys file
+       if ! test -d /home/cephadm/.ssh; then
+          mkdir /home/cephadm/.ssh
+          chown --reference /home/cephadm /home/cephadm/.ssh
+          chmod 0700 /home/cephadm/.ssh
+       fi
+       if ! test -e /home/cephadm/.ssh/authorized_keys; then
+          touch /home/cephadm/.ssh/authorized_keys
+          chown --reference /home/cephadm /home/cephadm/.ssh/authorized_keys
+          chmod 0600 /home/cephadm/.ssh/authorized_keys
+       fi
+
+    ;;
+    abort-upgrade|abort-remove|abort-deconfigure)
+       :
+    ;;
+
+    *)
+        echo "postinst called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/debian/cephadm.postrm b/debian/cephadm.postrm
new file mode 100644 (file)
index 0000000..379ad73
--- /dev/null
@@ -0,0 +1,44 @@
+#!/bin/sh
+# postrm script for cephadm
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <postrm> `remove'
+#        * <postrm> `purge'
+#        * <old-postrm> `upgrade' <new-version>
+#        * <new-postrm> `failed-upgrade' <old-version>
+#        * <new-postrm> `abort-install'
+#        * <new-postrm> `abort-install' <old-version>
+#        * <new-postrm> `abort-upgrade' <old-version>
+#        * <disappearer's-postrm> `disappear' <overwriter>
+#          <overwriter-version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+    remove)
+       deluser --remove-home cephadm
+    ;;
+
+    purge)
+    ;;
+
+    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+    ;;
+
+    *)
+        echo "postrm called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
index 6fd1e6c3f135d2607c3a545224b194a85b7702de..838b139fc76b64d9a16b85186776852c06c9579c 100644 (file)
@@ -167,16 +167,16 @@ Description: debugging symbols for ceph-base
  .
  This package contains the debugging symbols for ceph-base.
 
-Package: ceph-daemon
+Package: cephadm
 Architecture: linux-any
 Depends: docker.io,
         ${python:Depends},
-Description: ceph-daemon utility to bootstrap ceph daemons with systemd and containers
+Description: cephadm utility to bootstrap ceph daemons with systemd and containers
  Ceph is a massively scalable, open-source, distributed
  storage system that runs on commodity hardware and delivers object,
  block and file system storage.
  .
- The ceph-daemon utility is used to bootstrap a Ceph cluster and to manage
+ The cephadm utility is used to bootstrap a Ceph cluster and to manage
  ceph daemons deployed with systemd and containers.
 
 Package: ceph-mds
@@ -329,7 +329,7 @@ Description: kubernetes events plugin for ceph-mgr
 Package: ceph-mgr-ssh
 Architecture: all
 Depends: ceph-mgr (= ${binary:Version}),
-        ceph-daemon,
+        cephadm,
         python-six,
          ${misc:Depends},
          ${python:Depends},
index be3959c5313fedcfa0d05e37e44238a8aa79d476..be6b1539cd01ec152010a3ac075ebd76e4a062be 100755 (executable)
@@ -60,9 +60,9 @@ override_dh_auto_install:
        install -D -m 644 src/etc-rbdmap $(DESTDIR)/etc/ceph/rbdmap
        install -D -m 644 etc/sysctl/90-ceph-osd.conf $(DESTDIR)/etc/sysctl.d/30-ceph-osd.conf
        install -D -m 600 sudoers.d/ceph-osd-smartctl $(DESTDIR)/etc/sudoers.d/ceph-osd-smartctl
-       install -D -m 600 sudoers.d/cephdaemon $(DESTDIR)/etc/sudoers.d/cephdaemon
+       install -D -m 600 sudoers.d/cephadm $(DESTDIR)/etc/sudoers.d/cephadm
 
-       install -m 755 src/ceph-daemon/ceph-daemon $(DESTDIR)/usr/sbin/ceph-daemon
+       install -m 755 src/cephadm/cephadm $(DESTDIR)/usr/sbin/cephadm
 
 # doc/changelog is a directory, which confuses dh_installchangelogs
 override_dh_installchangelogs:
@@ -135,7 +135,7 @@ override_dh_python2:
        dh_python2 -p ceph-mgr
        # batch-compile, and set up for delete, all the module files
        dh_python2 -p ceph-mgr usr/lib/ceph/mgr
-       dh_python2 -p ceph-daemon
+       dh_python2 -p cephadm
 
 override_dh_python3:
        for binding in rados cephfs rbd rgw; do \
index c8a099098fd896963a68e331f9379f9a670c744e..c545563e12daaba3f672a3a91666ab7f42128941 100644 (file)
@@ -2,7 +2,7 @@
 ceph:
   deb:
   - ceph
-  - ceph-daemon
+  - cephadm
   - ceph-mds
   - ceph-mgr
   - ceph-common
@@ -35,7 +35,7 @@ ceph:
   - ceph-radosgw
   - ceph-test
   - ceph
-  - ceph-daemon
+  - cephadm
   - ceph-mgr
   - ceph-mgr-dashboard
   - ceph-mgr-diskprediction-cloud
index 1bb94f20c11dd5528ebbea0388f83557baf14ade..12d8c10eb338d7f2e2c7d42a3085fb00d4b76995 100755 (executable)
@@ -27,12 +27,12 @@ OSD_LV_NAME=${SCRIPT_NAME%.*}
 [ -z "$SUDO" ] && SUDO=sudo
 
 if [ -z "$CEPH_DAEMON" ]; then
-    CEPH_DAEMON=${SCRIPT_DIR}/../../src/ceph-daemon/ceph-daemon
+    CEPH_DAEMON=${SCRIPT_DIR}/../../src/cephadm/cephadm
 fi
 
 # at this point, we need $CEPH_DAEMON set
 if ! [ -x "$CEPH_DAEMON" ]; then
-    echo "ceph-daemon not found. Please set \$CEPH_DAEMON"
+    echo "cephadm not found. Please set \$CEPH_DAEMON"
     exit 1
 fi
 
@@ -42,7 +42,7 @@ if [ -z "$PYTHON_KLUDGE" ]; then
    TMPBINDIR=$(mktemp -d)
    trap "rm -rf $TMPBINDIR" EXIT
    ORIG_CEPH_DAEMON="$CEPH_DAEMON"
-   CEPH_DAEMON="$TMPBINDIR/ceph-daemon"
+   CEPH_DAEMON="$TMPBINDIR/cephadm"
    for p in $PYTHONS; do
        echo "=== re-running with $p ==="
        ln -s `which $p` $TMPBINDIR/python
@@ -211,7 +211,7 @@ for tarball in $TEST_TARS; do
         # validate after adopt
         out=$($CEPH_DAEMON ls | jq '.[]' \
                               | jq 'select(.name == "'$name'")')
-        echo $out | jq -r '.style' | grep 'ceph-daemon'
+        echo $out | jq -r '.style' | grep 'cephadm'
         echo $out | jq -r '.fsid' | grep $FSID_LEGACY
     done
     # clean-up before next iter
index c285e4604c63a4dfad13d0d005a0d7faf1b6461b..a102c5ae5e28e3002159314b1039f82230e38f2a 100644 (file)
@@ -11,7 +11,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - print: "**** done installing mimic"
 - ceph:
index c285e4604c63a4dfad13d0d005a0d7faf1b6461b..a102c5ae5e28e3002159314b1039f82230e38f2a 100644 (file)
@@ -11,7 +11,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - print: "**** done installing mimic"
 - ceph:
index 98f41f02322a3ac5598cb9c77fc9a51e40e987d3..35b26857dd4c41655bf5b6d0f23bc58053f42b3f 100644 (file)
@@ -2,4 +2,4 @@ overrides:
   ceph2:
     ceph_daemon_mode: packaged-ceph-daemon
   install:
-    extra_packages: [ceph-daemon]
+    extra_packages: [cephadm]
index 804a46e77322e9fb73af009c13e8b767a3ed4ded..d137fddcc4932c61344369a29801bec319a4def7 100644 (file)
@@ -21,7 +21,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
       - ceph-mgr
       - libcephfs2
       - libcephfs-devel
index 34935d967bf9a9c88f604aa61d044a1f6d906a34..ded6183bd684b5027983f178c8ec2a321ee1a14f 100644 (file)
@@ -18,7 +18,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
       - ceph-mgr
       - libcephfs2
       - libcephfs-devel
index a61c87d258b911aa37994f91842766b58dbd28d0..d8e0f7f2daf127abaecf51d25be33be8d9d246cb 100644 (file)
@@ -17,7 +17,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
       - ceph-mgr
       - libcephfs2
       - libcephfs-devel
index 9784952cabb2c8fdd6029037f7fd3c0bc57865b7..1ebe7af9c1466ff9ebcac48e5ecbdc52c8eaf958 100644 (file)
@@ -18,7 +18,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - install.upgrade:
     mon.a:
index 8f9d34d0b4565cc49177b1b37a07b1d4ced50215..ccd3c06a31afc630cd789d59e8835abccef55356 100644 (file)
@@ -17,7 +17,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - install.upgrade:
     mon.a:
index 34786ef784fac64db03dd77053564f90bd575748..4c503f42fe299a84f2274778b68bb35973315e6e 100644 (file)
@@ -18,7 +18,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - install.upgrade:
     mon.a:
index bdb6b62185576e71d044e98ed5c38163f3255786..20d4e00e213744286577c666af5b9f0537d8e6e4 100644 (file)
@@ -18,7 +18,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - install.upgrade:
     mon.a:
index 30e2ba2da525525308d661fadb1203e7f50b7c05..3ef5ec0366b39c2900349f00bde6ae515611cf21 100644 (file)
@@ -12,7 +12,7 @@ tasks:
 - install:
     branch: nautilus
     exclude_packages:
-      - ceph-daemon
+      - cephadm
 - install.upgrade:
     mon.a:
     mon.b:
index 4db075e79c1293122bc2ac6aee62b3b3f8653b3b..4077e07d01b0c34d0747f90ed9ff193527b1527b 100644 (file)
@@ -11,7 +11,7 @@ tasks:
 - install:
     branch: nautilus
     exclude_packages:
-      - ceph-daemon
+      - cephadm
 - install.upgrade:
     mon.a:
     mon.b:
index 3d2691a7b1ddccccf70a6be0cbc2be96bfbd304c..ed38c9c9003813379e752d3b9bbea6012fd92011 100644 (file)
@@ -6,7 +6,7 @@ tasks:
 - install:
     branch: nautilus
     exclude_packages:
-      - ceph-daemon
+      - cephadm
 - install.upgrade:
     mon.a:
     mon.b:
index 02ac0468e23e457aeffed6264d43a6ea4daf00e7..7d0040c3ecf278af7dbd41d4e13ecdc9cfa19907 100644 (file)
@@ -16,7 +16,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - print: "**** done install mimic"
 - ceph:
index e4ca466baeb0c49bd645fec5ef9183f67b3e9b92..bed8225377293ee21eca5a2bb21d8868bb8562eb 100644 (file)
@@ -13,7 +13,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - print: "**** done installing mimic"
 - ceph:
index 6c4fe67e8fe682cb0af6aca96e66308131cb94e3..835a20fdb74607517b3514ebb1010f53e7d43b5c 100644 (file)
@@ -10,7 +10,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - print: "**** done install mimic"
 - ceph:
index e56c437e0a1c12a5c803aa86d8a8f99f3e397139..064c2203cfd39b558ff79224fd9137371cf58330 100644 (file)
@@ -16,7 +16,7 @@ tasks:
       - ceph-mgr-diskprediction-cloud
       - ceph-mgr-rook
       - ceph-mgr-ssh
-      - ceph-daemon
+      - cephadm
     extra_packages: ['librados2']
 - print: "**** done install nautilus"
 - ceph:
index c9ef661c0dc9496545de028690eb5f88f507ba9a..27b291ca38bf70ff7427e19b1d0f418e77728167 100644 (file)
@@ -98,15 +98,15 @@ def download_ceph_daemon(ctx, config, ref):
     if config.get('ceph_daemon_mode') != 'packaged-ceph-daemon':
         ref = config.get('ceph_daemon_branch', ref)
         git_url = teuth_config.get_ceph_git_url()
-        log.info('Downloading ceph-daemon (repo %s ref %s)...' % (git_url, ref))
+        log.info('Downloading cephadm (repo %s ref %s)...' % (git_url, ref))
         ctx.cluster.run(
             args=[
                 'git', 'archive',
                 '--remote=' + git_url,
                 ref,
-                'src/ceph-daemon/ceph-daemon',
+                'src/cephadm/cephadm',
                 run.Raw('|'),
-                'tar', '-xO', 'src/ceph-daemon/ceph-daemon',
+                'tar', '-xO', 'src/cephadm/cephadm',
                 run.Raw('>'),
                 ctx.ceph_daemon,
                 run.Raw('&&'),
@@ -131,7 +131,7 @@ def download_ceph_daemon(ctx, config, ref):
         ])
 
         if config.get('ceph_daemon_mode') == 'root':
-            log.info('Removing ceph-daemon ...')
+            log.info('Removing cephadm ...')
             ctx.cluster.run(
                 args=[
                     'rm',
@@ -784,9 +784,9 @@ def task(ctx, config):
         config['ceph_daemon_mode'] = 'root'
     assert config['ceph_daemon_mode'] in ['root', 'packaged-ceph-daemon']
     if config['ceph_daemon_mode'] == 'root':
-        ctx.ceph_daemon = testdir + '/ceph-daemon'
+        ctx.ceph_daemon = testdir + '/cephadm'
     else:
-        ctx.ceph_daemon = 'ceph-daemon'  # in the path
+        ctx.ceph_daemon = 'cephadm'  # in the path
 
     if first_ceph_cluster:
         # FIXME: this is global for all clusters
index 8b363766b9076e17e0b7ff726b2463f2105e676a..08c9b094c70573783e44f1179db139373fe83759 100644 (file)
@@ -509,7 +509,7 @@ endif()
 add_subdirectory(pybind)
 add_subdirectory(ceph-volume)
 add_subdirectory(python-common)
-add_subdirectory(ceph-daemon)
+add_subdirectory(cephadm)
 
 # Monitor
 add_subdirectory(mon)
diff --git a/src/ceph-daemon/CMakeLists.txt b/src/ceph-daemon/CMakeLists.txt
deleted file mode 100644 (file)
index a83105b..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-if(WITH_TESTS)
-  include(AddCephTest)
-  add_tox_test(ceph-daemon TOX_ENVS mypy)
-endif()
diff --git a/src/ceph-daemon/ceph-daemon b/src/ceph-daemon/ceph-daemon
deleted file mode 100755 (executable)
index 2a9b0e0..0000000
+++ /dev/null
@@ -1,2131 +0,0 @@
-#!/usr/bin/python
-
-DEFAULT_IMAGE='ceph/daemon-base:latest-master-devel'  # FIXME when octopus is ready!!!
-DATA_DIR='/var/lib/ceph'
-LOG_DIR='/var/log/ceph'
-LOGROTATE_DIR='/etc/logrotate.d'
-UNIT_DIR='/etc/systemd/system'
-LOG_DIR_MODE=0o770
-DATA_DIR_MODE=0o700
-CONTAINER_PREFERENCE = ['podman', 'docker']  # prefer podman to docker
-CUSTOM_PS1=r'[ceph: \u@\h \W]\$ '
-
-"""
-You can invoke ceph-daemon in two ways:
-
-1. The normal way, at the command line.
-
-2. By piping the script to the python3 binary.  In this latter case, you should
-   prepend one or more lines to the beginning of the script.
-
-   For arguments,
-
-       injected_argv = [...]
-
-   e.g.,
-
-       injected_argv = ['ls']
-
-   For reading stdin from the '--config-and-json -' argument,
-
-       injected_stdin = '...'
-"""
-
-import argparse
-try:
-    from ConfigParser import ConfigParser   # py2
-except ImportError:
-    from configparser import ConfigParser   # py3
-import fcntl
-try:
-    from StringIO import StringIO           # py2
-except ImportError:
-    from io import StringIO                 # py3
-import json
-import logging
-import os
-import random
-import re
-import select
-import shutil
-import socket
-import string
-import subprocess
-import sys
-import tempfile
-import time
-try:
-    from typing import Dict, List, Tuple, Optional, Union
-except ImportError:
-    pass
-import uuid
-
-from distutils.spawn import find_executable
-from functools import wraps
-from glob import glob
-
-
-container_path = None
-
-class Error(Exception):
-    pass
-
-##################################
-# Popen wrappers, lifted from ceph-volume
-
-def call(command, desc=None, verbose=False, **kwargs):
-    """
-    Wrap subprocess.Popen to
-
-    - log stdout/stderr to a logger,
-    - decode utf-8
-    - cleanly return out, err, returncode
-
-    If verbose=True, log at info (instead of debug) level.
-
-    :param verbose_on_failure: On a non-zero exit status, it will forcefully set
-                               logging ON for the terminal
-    """
-    if not desc:
-        desc = command[0]
-    verbose_on_failure = kwargs.pop('verbose_on_failure', True)
-
-    logger.debug("Running command: %s" % ' '.join(command))
-    process = subprocess.Popen(
-        command,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-        close_fds=True,
-        **kwargs
-    )
-    # get current p.stdout flags, add O_NONBLOCK
-    stdout_flags = fcntl.fcntl(process.stdout, fcntl.F_GETFL)
-    stderr_flags = fcntl.fcntl(process.stderr, fcntl.F_GETFL)
-    fcntl.fcntl(process.stdout, fcntl.F_SETFL, stdout_flags | os.O_NONBLOCK)
-    fcntl.fcntl(process.stderr, fcntl.F_SETFL, stderr_flags | os.O_NONBLOCK)
-
-    out = ''
-    err = ''
-    reads = None
-    stop = False
-    out_buffer = ''   # partial line (no newline yet)
-    err_buffer = ''   # partial line (no newline yet)
-    while not stop:
-        if reads and process.poll() is not None:
-            # we want to stop, but first read off anything remaining
-            # on stdout/stderr
-            stop = True
-        else:
-            reads, _, _ = select.select(
-                [process.stdout.fileno(), process.stderr.fileno()],
-                [], []
-            )
-        for fd in reads:
-            try:
-                message = os.read(fd, 1024)
-                if not isinstance(message, str):
-                    message = message.decode('utf-8')
-                if fd == process.stdout.fileno():
-                    out += message
-                    message = out_buffer + message
-                    lines = message.split('\n')
-                    out_buffer = lines.pop()
-                    for line in lines:
-                        if verbose:
-                            logger.info(desc + ':stdout ' + line)
-                        else:
-                            logger.debug(desc + ':stdout ' + line)
-                elif fd == process.stderr.fileno():
-                    err += message
-                    message = err_buffer + message
-                    lines = message.split('\n')
-                    err_buffer = lines.pop()
-                    for line in lines:
-                        if verbose:
-                            logger.info(desc + ':stderr ' + line)
-                        else:
-                            logger.debug(desc + ':stderr ' + line)
-                else:
-                    assert False
-            except (IOError, OSError):
-                pass
-
-    returncode = process.wait()
-
-    if out_buffer != '':
-        if verbose:
-            logger.info(desc + ':stdout ' + out_buffer)
-        else:
-            logger.debug(desc + ':stdout ' + out_buffer)
-    if err_buffer != '':
-        if verbose:
-            logger.info(desc + ':stderr ' + err_buffer)
-        else:
-            logger.debug(desc + ':stderr ' + err_buffer)
-
-    if returncode != 0 and verbose_on_failure and not verbose:
-        # dump stdout + stderr
-        logger.info('Non-zero exit code %d from %s' % (returncode, ' '.join(command)))
-        for line in out.splitlines():
-            logger.info(desc + ':stdout ' + line)
-        for line in err.splitlines():
-            logger.info(desc + ':stderr ' + line)
-
-    return out, err, returncode
-
-def call_throws(command, **kwargs):
-    out, err, ret = call(command, **kwargs)
-    if ret:
-        raise RuntimeError('Failed command: %s' % ' '.join(command))
-    return out, err, ret
-
-##################################
-
-def read_config(fn):
-    # type: (Optional[str]) -> ConfigParser
-    # bend over backwards here because py2's ConfigParser doesn't like
-    # whitespace before config option names (e.g., '\n foo = bar\n').
-    # Yeesh!
-    cp = ConfigParser()
-    if fn:
-        with open(fn, 'r') as f:
-            raw_conf = f.read()
-        nice_conf = re.sub('\n(\s)+', '\n', raw_conf)
-        cp.readfp(StringIO(nice_conf))
-    return cp
-
-def pathify(p):
-    # type: (str) -> str
-    if not p.startswith('/'):
-        return os.path.join(os.getcwd(), p)
-    return p
-
-def get_hostname():
-    # type: () -> str
-    return socket.gethostname()
-
-def get_fqdn():
-    # type: () -> str
-    return socket.getfqdn() or socket.gethostname()
-
-def generate_service_id():
-    # type: () -> str
-    return ''.join(random.choice(string.ascii_lowercase)
-                   for _ in range(6))
-
-def generate_password():
-    # type: () -> str
-    return ''.join(random.choice(string.ascii_lowercase + string.digits)
-                   for i in range(10))
-
-def make_fsid():
-    # type: () -> str
-    return str(uuid.uuid1())
-
-def is_fsid(s):
-    # type: (str) -> bool
-    try:
-        uuid.UUID(s)
-    except ValueError:
-        return False
-    return True
-
-def infer_fsid(func):
-    """
-    If we only find a single fsid in /var/lib/ceph/*, use that
-    """
-    @wraps(func)
-    def _infer_fsid():
-        if args.fsid:
-            logger.debug('Using specified fsid: %s' % args.fsid)
-            return func()
-
-        fsid_list = []
-        if os.path.exists(args.data_dir):
-            for i in os.listdir(args.data_dir):
-                if is_fsid(i):
-                    fsid_list.append(i)
-        logger.debug('Found fsids %s' % str(fsid_list))
-
-        if not fsid_list:
-            # TODO: raise?
-            return func()
-
-        if len(fsid_list) > 1:
-            raise Error('cannot infer fsid, must specify --fsid')
-
-        logger.info('Inferring fsid %s' % fsid_list[0])
-        args.fsid = fsid_list[0]
-        return func()
-    return _infer_fsid
-
-def write_tmp(s, uid, gid):
-    tmp_f = tempfile.NamedTemporaryFile(mode='w',
-                                        prefix='ceph-tmp')
-    os.fchown(tmp_f.fileno(), uid, gid)
-    tmp_f.write(s)
-    tmp_f.flush()
-
-    return tmp_f
-
-def makedirs(dir, uid, gid, mode):
-    # type: (str, int, int, int) -> None
-    if not os.path.exists(dir):
-        os.makedirs(dir, mode=mode)
-    else:
-        os.chmod(dir, mode)
-    os.chown(dir, uid, gid)
-    os.chmod(dir, mode)   # the above is masked by umask...
-
-def get_data_dir(fsid, t, n):
-    # type: (str, str, Union[int, str]) -> str
-    return os.path.join(args.data_dir, fsid, '%s.%s' % (t, n))
-
-def get_log_dir(fsid):
-    # type: (str) -> str
-    return os.path.join(args.log_dir, fsid)
-
-def make_data_dir_base(fsid, uid, gid):
-    # type: (str, int, int) -> str
-    data_dir_base = os.path.join(args.data_dir, fsid)
-    makedirs(data_dir_base, uid, gid, DATA_DIR_MODE)
-    makedirs(os.path.join(data_dir_base, 'crash'), uid, gid, DATA_DIR_MODE)
-    makedirs(os.path.join(data_dir_base, 'crash', 'posted'), uid, gid,
-             DATA_DIR_MODE)
-    return data_dir_base
-
-def make_data_dir(fsid, daemon_type, daemon_id, uid=None, gid=None):
-    # type: (str, str, Union[int, str], int, int) -> str
-    if not uid or not gid:
-        (uid, gid) = extract_uid_gid()
-    make_data_dir_base(fsid, uid, gid)
-    data_dir = get_data_dir(fsid, daemon_type, daemon_id)
-    makedirs(data_dir, uid, gid, DATA_DIR_MODE)
-    return data_dir
-
-def make_log_dir(fsid, uid=None, gid=None):
-    # type: (str, int, int) -> str
-    if not uid or not gid:
-        (uid, gid) = extract_uid_gid()
-    log_dir = get_log_dir(fsid)
-    makedirs(log_dir, uid, gid, LOG_DIR_MODE)
-    return log_dir
-
-def copy_files(src, dst, uid=None, gid=None):
-    # type: (List[str], str, int, int) -> None
-    """
-    Copy a files from src to dst
-    """
-    if not uid or not gid:
-        (uid, gid) = extract_uid_gid()
-
-    for src_file in src:
-        dst_file = dst
-        if os.path.isdir(dst):
-            dst_file = os.path.join(dst, os.path.basename(src_file))
-
-        logger.debug('copy file \'%s\' -> \'%s\'' % (src_file, dst_file))
-        shutil.copyfile(src_file, dst_file)
-
-        logger.debug('chown %s:%s \'%s\'' % (uid, gid, dst_file))
-        os.chown(dst_file, uid, gid)
-
-def move_files(src, dst, uid=None, gid=None):
-    # type: (List[str], str, int, int) -> None
-    """
-    Move files from src to dst
-    """
-    if not uid or not gid:
-        (uid, gid) = extract_uid_gid()
-
-    for src_file in src:
-        dst_file = dst
-        if os.path.isdir(dst):
-            dst_file = os.path.join(dst, os.path.basename(src_file))
-
-        if os.path.islink(src_file):
-            # shutil.move() in py2 does not handle symlinks correctly
-            src_rl = os.readlink(src_file)
-            logger.debug("symlink '%s' -> '%s'" % (dst_file, src_rl))
-            os.symlink(src_rl, dst_file)
-            os.unlink(src_file)
-        else:
-            logger.debug("move file '%s' -> '%s'" % (src_file, dst_file))
-            shutil.move(src_file, dst_file)
-            logger.debug('chown %s:%s \'%s\'' % (uid, gid, dst_file))
-            os.chown(dst_file, uid, gid)
-
-def find_program(filename):
-    # type: (str) -> str
-    name = find_executable(filename)
-    if name is None:
-        raise ValueError('%s not found' % filename)
-    return name
-
-def get_unit_name(fsid, daemon_type, daemon_id=None):
-    # type (str, str, Optional[Union[int, str]]) -> str
-    # accept either name or type + id
-    if daemon_id is not None:
-        return 'ceph-%s@%s.%s' % (fsid, daemon_type, daemon_id)
-    else:
-        return 'ceph-%s@%s' % (fsid, daemon_type)
-
-def check_unit(unit_name):
-    # type: (str) -> Tuple[bool, str]
-    # NOTE: we ignore the exit code here because systemctl outputs
-    # various exit codes based on the state of the service, but the
-    # string result is more explicit (and sufficient).
-    enabled = False
-    try:
-        out, err, code = call(['systemctl', 'is-enabled', unit_name],
-                              verbose_on_failure=False)
-        if code == 0:
-            enabled = True
-    except Exception as e:
-        logger.warning('unable to run systemctl: %s' % e)
-        enabled = False
-
-    state = 'unknown'
-    try:
-        out, err, code = call(['systemctl', 'is-active', unit_name],
-                              verbose_on_failure=False)
-        out = out.strip()
-        if out in ['active']:
-            state = 'running'
-        elif out in ['inactive']:
-            state = 'stopped'
-        elif out in ['failed', 'auto-restart']:
-            state = 'error'
-        else:
-            state = 'unknown'
-    except Exception as e:
-        logger.warning('unable to run systemctl: %s' % e)
-        state = 'unknown'
-    return (enabled, state)
-
-def get_legacy_config_fsid(cluster, legacy_dir=None):
-    # type: (str, str) -> Optional[str]
-    config_file = '/etc/ceph/%s.conf' % cluster
-    if legacy_dir is not None:
-        config_file = os.path.abspath(legacy_dir + config_file)
-
-    config = read_config(config_file)
-
-    if config.has_section('global') and config.has_option('global', 'fsid'):
-        return config.get('global', 'fsid')
-    return None
-
-def get_legacy_daemon_fsid(cluster, daemon_type, daemon_id, legacy_dir=None):
-    # type: (str, str, Union[int, str], str) -> Optional[str]
-    fsid = None
-    if daemon_type == 'osd':
-        try:
-            fsid_file = os.path.join(args.data_dir,
-                                     daemon_type,
-                                     'ceph-%s' % daemon_id,
-                                     'ceph_fsid')
-            if legacy_dir is not None:
-                fsid_file = os.path.abspath(legacy_dir + fsid_file)
-            with open(fsid_file, 'r') as f:
-                fsid = f.read().strip()
-        except IOError:
-            pass
-    if not fsid:
-        fsid = get_legacy_config_fsid(cluster, legacy_dir=legacy_dir)
-    return fsid
-
-def get_daemon_args(fsid, daemon_type, daemon_id):
-    # type: (str, str, Union[int, str]) -> List[str]
-    r = [
-        '--default-log-to-file=false',
-        '--default-log-to-stderr=true',
-        ]
-    r += ['--setuser', 'ceph']
-    r += ['--setgroup', 'ceph']
-    return r
-
-def create_daemon_dirs(fsid, daemon_type, daemon_id, uid, gid,
-                       config=None, keyring=None):
-    # type: (str, str, Union[int, str], int, int, str, str) ->  None
-    data_dir = make_data_dir(fsid, daemon_type, daemon_id, uid=uid, gid=gid)
-    make_log_dir(fsid)
-
-    if config:
-        with open(data_dir + '/config', 'w') as f:
-            os.fchown(f.fileno(), uid, gid)
-            os.fchmod(f.fileno(), 0o600)
-            f.write(config)
-    if keyring:
-        with open(data_dir + '/keyring', 'w') as f:
-            os.fchmod(f.fileno(), 0o600)
-            os.fchown(f.fileno(), uid, gid)
-            f.write(keyring)
-
-def get_config_and_keyring():
-    # type: () -> Tuple[str, str]
-    if args.config_and_keyring:
-        if args.config_and_keyring == '-':
-            try:
-                j = injected_stdin # type: ignore
-            except NameError:
-                j = sys.stdin.read()
-        else:
-            with open(args.config_and_keyring, 'r') as f:
-                j = f.read()
-        d = json.loads(j)
-        config = d.get('config')
-        keyring = d.get('keyring')
-    else:
-        if args.key:
-            keyring = '[%s]\n\tkey = %s\n' % (args.name, args.key)
-        elif args.keyring:
-            with open(args.keyring, 'r') as f:
-                keyring = f.read()
-        else:
-            raise Error('no keyring provided')
-        with open(args.config, 'r') as f:
-            config = f.read()
-    return (config, keyring)
-
-def get_config_and_both_keyrings():
-    # type: () -> Tuple[str, str, Optional[str]]
-    if args.config_and_keyrings:
-        if args.config_and_keyrings == '-':
-            try:
-                j = injected_stdin # type: ignore
-            except NameError:
-                j = sys.stdin.read()
-        else:
-            with open(args.config_and_keyrings, 'r') as f:
-                j = f.read()
-        d = json.loads(j)
-        return (d.get('config'), d.get('keyring'), d.get('crash_keyring'))
-    else:
-        if args.key:
-            keyring = '[%s]\n\tkey = %s\n' % (args.name, args.key)
-        elif args.keyring:
-            with open(args.keyring, 'r') as f:
-                keyring = f.read()
-        else:
-            raise Error('no keyring provided')
-        crash_keyring = None
-        if args.crash_keyring:
-            with open(args.crash_keyring, 'r') as f:
-                crash_keyring = f.read()
-        with open(args.config, 'r') as f:
-            config = f.read()
-        return (config, keyring, crash_keyring)
-
-def get_container_mounts(fsid, daemon_type, daemon_id):
-    # type: (str, str, Union[int, str, None]) -> Dict[str, str]
-    mounts = {}
-    if fsid:
-        run_path = os.path.join('/var/run/ceph', fsid);
-        if os.path.exists(run_path):
-            mounts[run_path] = '/var/run/ceph:z'
-        log_dir = get_log_dir(fsid)
-        mounts[log_dir] = '/var/log/ceph:z'
-        crash_dir = '/var/lib/ceph/%s/crash' % fsid
-        if os.path.exists(crash_dir):
-            mounts[crash_dir] = '/var/lib/ceph/crash:z'
-
-    if daemon_id:
-        data_dir = get_data_dir(fsid, daemon_type, daemon_id)
-        if daemon_type == 'rgw':
-            cdata_dir = '/var/lib/ceph/radosgw/ceph-rgw.%s' % (daemon_id)
-        else:
-            cdata_dir = '/var/lib/ceph/%s/ceph-%s' % (daemon_type, daemon_id)
-        mounts[data_dir] = cdata_dir + ':z'
-        mounts[data_dir + '/config'] = '/etc/ceph/ceph.conf:z'
-        if daemon_type == 'rbd-mirror':
-            # rbd-mirror does not search for its keyring in a data directory
-            mounts[data_dir + '/keyring'] = '/etc/ceph/ceph.client.rbd-mirror.%s.keyring' % daemon_id
-
-    if daemon_type in ['mon', 'osd']:
-        mounts['/dev'] = '/dev'  # FIXME: narrow this down?
-        mounts['/run/udev'] = '/run/udev'
-    if daemon_type == 'osd':
-        mounts['/sys'] = '/sys'  # for numa.cc, pick_address, cgroups, ...
-        mounts['/run/lvm'] = '/run/lvm'
-        mounts['/run/lock/lvm'] = '/run/lock/lvm'
-
-    return mounts
-
-def get_container(fsid, daemon_type, daemon_id, privileged=False,
-                  container_args=[]):
-    # type: (str, str, Union[int, str], bool, List[str]) -> CephContainer
-    if daemon_type in ['mon', 'osd'] or privileged:
-        # mon and osd need privileged in order for libudev to query devices
-        container_args += ['--privileged']
-    if daemon_type == 'rgw':
-        entrypoint = '/usr/bin/radosgw'
-        name = 'client.rgw.%s' % daemon_id
-    elif daemon_type == 'rbd-mirror':
-        entrypoint = '/usr/bin/rbd-mirror'
-        name = 'client.rbd-mirror.%s' % daemon_id
-    else:
-        entrypoint = '/usr/bin/ceph-' + daemon_type
-        name = '%s.%s' % (daemon_type, daemon_id)
-    return CephContainer(
-        image=args.image,
-        entrypoint=entrypoint,
-        args=[
-            '-n', name,
-            '-f', # foreground
-        ] + get_daemon_args(fsid, daemon_type, daemon_id),
-        container_args=container_args,
-        volume_mounts=get_container_mounts(fsid, daemon_type, daemon_id),
-        cname='ceph-%s-%s.%s' % (fsid, daemon_type, daemon_id),
-    )
-
-def extract_uid_gid():
-    # type: () -> Tuple[int, int]
-    out = CephContainer(
-        image=args.image,
-        entrypoint='/usr/bin/grep',
-        args=['^ceph:', '/etc/passwd'],
-    ).run()
-    (uid, gid) = out.split(':')[2:4]
-    return (int(uid), int(gid))
-
-def deploy_daemon(fsid, daemon_type, daemon_id, c, uid, gid,
-                  config, keyring):
-    # type: (str, str, Union[int, str], CephContainer, int, int, Optional[str], Optional[str]) -> None
-    if daemon_type == 'mon' and not os.path.exists(
-            get_data_dir(fsid, 'mon', daemon_id)):
-        assert config
-        assert keyring
-        # tmp keyring file
-        tmp_keyring = write_tmp(keyring, uid, gid)
-
-        # tmp config file
-        tmp_config = write_tmp(config, uid, gid)
-
-        # --mkfs
-        create_daemon_dirs(fsid, daemon_type, daemon_id, uid, gid)
-        mon_dir = get_data_dir(fsid, 'mon', daemon_id)
-        log_dir = get_log_dir(fsid)
-        out = CephContainer(
-            image=args.image,
-            entrypoint='/usr/bin/ceph-mon',
-            args=['--mkfs',
-                  '-i', str(daemon_id),
-                  '--fsid', fsid,
-                  '-c', '/tmp/config',
-                  '--keyring', '/tmp/keyring',
-            ] + get_daemon_args(fsid, 'mon', daemon_id),
-            volume_mounts={
-                log_dir: '/var/log/ceph:z',
-                mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % (daemon_id),
-                tmp_keyring.name: '/tmp/keyring:z',
-                tmp_config.name: '/tmp/config:z',
-            },
-        ).run()
-
-        # write conf
-        with open(mon_dir + '/config', 'w') as f:
-            os.fchown(f.fileno(), uid, gid)
-            os.fchmod(f.fileno(), 0o600)
-            f.write(config)
-    else:
-        # dirs, conf, keyring
-        create_daemon_dirs(
-            fsid, daemon_type, daemon_id,
-            uid, gid,
-            config, keyring)
-
-    if daemon_type == 'osd' and args.osd_fsid:
-        pc = CephContainer(
-            image=args.image,
-            entrypoint='/usr/sbin/ceph-volume',
-            args=[
-                'lvm', 'activate',
-                str(daemon_id), args.osd_fsid,
-                '--no-systemd'
-            ],
-            container_args=['--privileged'],
-            volume_mounts=get_container_mounts(fsid, daemon_type, daemon_id),
-            cname='ceph-%s-activate-%s.%s' % (fsid, daemon_type, daemon_id),
-        )
-        pc.run()
-
-    deploy_daemon_units(fsid, uid, gid, daemon_type, daemon_id, c)
-    update_firewalld(daemon_type)
-
-def deploy_daemon_units(fsid, uid, gid, daemon_type, daemon_id, c,
-                        enable=True, start=True):
-    # type: (str, int, int, str, Union[int, str], CephContainer, bool, bool) -> None
-    # cmd
-    data_dir = get_data_dir(fsid, daemon_type, daemon_id)
-    with open(data_dir + '/cmd', 'w') as f:
-        f.write('#!/bin/sh\n' + ' '.join(c.run_cmd()) + '\n')
-        os.fchmod(f.fileno(), 0o700)
-
-    # systemd
-    install_base_units(fsid)
-    unit = get_unit_file(fsid, uid, gid)
-    unit_file = 'ceph-%s@.service' % (fsid)
-    with open(args.unit_dir + '/' + unit_file + '.new', 'w') as f:
-        f.write(unit)
-        os.rename(args.unit_dir + '/' + unit_file + '.new',
-                  args.unit_dir + '/' + unit_file)
-    call_throws(['systemctl', 'daemon-reload'])
-
-    unit_name = get_unit_name(fsid, daemon_type, daemon_id)
-    call(['systemctl', 'stop', unit_name],
-         verbose_on_failure=False)
-    call(['systemctl', 'reset-failed', unit_name],
-         verbose_on_failure=False)
-    if enable:
-        call_throws(['systemctl', 'enable', unit_name])
-    if start:
-        call_throws(['systemctl', 'start', unit_name])
-
-def update_firewalld(daemon_type):
-    if args.skip_firewalld:
-        return
-    cmd = find_executable('firewall-cmd')
-    if not cmd:
-        logger.debug('firewalld does not appear to be present')
-        return
-    (enabled, state) = check_unit('firewalld.service')
-    if not enabled:
-        logger.debug('firewalld.service is not enabled')
-        return
-
-    fw_services = []
-    fw_ports = []
-    if daemon_type == 'mon':
-        fw_services.append('ceph-mon')
-    elif daemon_type in ['mgr', 'mds', 'osd']:
-        fw_services.append('ceph')
-    if daemon_type == 'mgr':
-        fw_ports.append(8080)  # dashboard
-        fw_ports.append(8443)  # dashboard
-        fw_ports.append(9283)  # prometheus
-
-    for svc in fw_services:
-        out, err, ret = call([cmd, '--permanent', '--query-service', svc])
-        if ret:
-            logger.info('Enabling firewalld service %s in current zone...' % svc)
-            out, err, ret = call([cmd, '--permanent', '--add-service', svc])
-            if ret:
-                raise RuntimeError(
-                    'unable to add service %s to current zone: %s' % (svc, err))
-        else:
-            logger.debug('firewalld service %s is enabled in current zone' % svc)
-    for port in fw_ports:
-        port = str(port) + '/tcp'
-        out, err, ret = call([cmd, '--permanent', '--query-port', port])
-        if ret:
-            logger.info('Enabling firewalld port %s in current zone...' % port)
-            out, err, ret = call([cmd, '--permanent', '--add-port', port])
-            if ret:
-                raise RuntimeError('unable to add port %s to current zone: %s' %
-                                   (port, err))
-        else:
-            logger.debug('firewalld port %s is enabled in current zone' % port)
-    call_throws([cmd, '--reload'])
-
-def install_base_units(fsid):
-    # type: (str) -> None
-    """
-    Set up ceph.target and ceph-$fsid.target units.
-    """
-    # global unit
-    existed = os.path.exists(args.unit_dir + '/ceph.target')
-    with open(args.unit_dir + '/ceph.target.new', 'w') as f:
-        f.write('[Unit]\n'
-                'Description=All Ceph clusters and services\n'
-                '\n'
-                '[Install]\n'
-                'WantedBy=multi-user.target\n')
-        os.rename(args.unit_dir + '/ceph.target.new',
-                  args.unit_dir + '/ceph.target')
-    if not existed:
-        # we disable before enable in case a different ceph.target
-        # (from the traditional package) is present; while newer
-        # systemd is smart enough to disable the old
-        # (/lib/systemd/...) and enable the new (/etc/systemd/...),
-        # some older versions of systemd error out with EEXIST.
-        call_throws(['systemctl', 'disable', 'ceph.target'])
-        call_throws(['systemctl', 'enable', 'ceph.target'])
-        call_throws(['systemctl', 'start', 'ceph.target'])
-
-    # cluster unit
-    existed = os.path.exists(args.unit_dir + '/ceph-%s.target' % fsid)
-    with open(args.unit_dir + '/ceph-%s.target.new' % fsid, 'w') as f:
-        f.write('[Unit]\n'
-                'Description=Ceph cluster {fsid}\n'
-                'PartOf=ceph.target\n'
-                'Before=ceph.target\n'
-                '\n'
-                '[Install]\n'
-                'WantedBy=multi-user.target ceph.target\n'.format(
-                    fsid=fsid)
-        )
-        os.rename(args.unit_dir + '/ceph-%s.target.new' % fsid,
-                  args.unit_dir + '/ceph-%s.target' % fsid)
-    if not existed:
-        call_throws(['systemctl', 'enable', 'ceph-%s.target' % fsid])
-        call_throws(['systemctl', 'start', 'ceph-%s.target' % fsid])
-
-    # logrotate for the cluster
-    with open(args.logrotate_dir + '/ceph-%s' % fsid, 'w') as f:
-        """
-        This is a bit sloppy in that the killall/pkill will touch all ceph daemons
-        in all containers, but I don't see an elegant way to send SIGHUP *just* to
-        the daemons for this cluster.  (1) systemd kill -s will get the signal to
-        podman, but podman will exit.  (2) podman kill will get the signal to the
-        first child (bash), but that isn't the ceph daemon.  This is simpler and
-        should be harmless.
-        """
-        f.write("""# created by ceph-daemon
-/var/log/ceph/%s/*.log {
-    rotate 7
-    daily
-    compress
-    sharedscripts
-    postrotate
-        killall -q -1 ceph-mon ceph-mgr ceph-mds ceph-osd ceph-fuse radosgw || pkill -1 -x "ceph-mon|ceph-mgr|ceph-mds|ceph-osd|ceph-fuse|radosgw" || true
-    endscript
-    missingok
-    notifempty
-    su root root
-}
-""" % fsid)
-
-def deploy_crash(fsid, uid, gid, config, keyring):
-    # type: (str, int, int, str, str) -> None
-    crash_dir = os.path.join(args.data_dir, fsid, 'crash')
-    makedirs(crash_dir, uid, gid, DATA_DIR_MODE)
-
-    with open(os.path.join(crash_dir, 'keyring'), 'w') as f:
-        os.fchmod(f.fileno(), 0o600)
-        os.fchown(f.fileno(), uid, gid)
-        f.write(keyring)
-    with open(os.path.join(crash_dir, 'config'), 'w') as f:
-        os.fchmod(f.fileno(), 0o600)
-        os.fchown(f.fileno(), uid, gid)
-        f.write(config)
-
-    # ceph-crash unit
-    mounts = {
-        crash_dir: '/var/lib/ceph/crash:z',
-        os.path.join(crash_dir, 'config'): '/etc/ceph/ceph.conf:z',
-        os.path.join(crash_dir, 'keyring'): '/etc/ceph/ceph.keyring:z',
-    }
-    c = CephContainer(
-        image=args.image,
-        entrypoint='/usr/bin/ceph-crash',
-        args=['-n', 'client.crash.%s' % get_hostname()],
-        volume_mounts=mounts,
-        cname='ceph-%s-crash' % (fsid),
-    )
-    unit_name = 'ceph-%s-crash.service' % fsid
-    with open(os.path.join(args.unit_dir, unit_name + '.new'), 'w') as f:
-        f.write('[Unit]\n'
-                'Description=Ceph cluster {fsid} crash dump collector\n'
-                'PartOf=ceph-{fsid}.target\n'
-                'Before=ceph-{fsid}.target\n'
-                '\n'
-                '[Service]\n'
-                'Type=simple\n'
-                'ExecStart={cmd}\n'
-                'ExecStop=-{container_path} rm -f ceph-{fsid}-crash\n'
-                'Restart=always\n'
-                'RestartSec=10\n'
-                'StartLimitInterval=10min\n'
-                'StartLimitBurst=10\n'
-                '\n'
-                '[Install]\n'
-                'WantedBy=ceph-{fsid}.target\n'.format(
-                    container_path=container_path,
-                    fsid=fsid,
-                    cmd=' '.join(c.run_cmd()))
-        )
-        os.rename(os.path.join(args.unit_dir, unit_name + '.new'),
-                  os.path.join(args.unit_dir, unit_name))
-    subprocess.check_output(['systemctl', 'enable', unit_name])
-    subprocess.check_output(['systemctl', 'start', unit_name])
-
-def get_unit_file(fsid, uid, gid):
-    # type: (str, int, int) -> str
-    install_path = find_program('install')
-    u = """[Unit]
-Description=Ceph daemon for {fsid}
-
-# According to:
-#   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
-Wants=network-online.target local-fs.target time-sync.target
-
-PartOf=ceph-{fsid}.target
-Before=ceph-{fsid}.target
-
-[Service]
-LimitNOFILE=1048576
-LimitNPROC=1048576
-EnvironmentFile=-/etc/environment
-ExecStartPre=-{container_path} rm ceph-{fsid}-%i
-ExecStartPre=-{install_path} -d -m0770 -o {uid} -g {gid} /var/run/ceph/{fsid}
-ExecStart=/bin/bash {data_dir}/{fsid}/%i/cmd
-ExecStop=-{container_path} rm -f ceph-{fsid}-%i
-Restart=on-failure
-RestartSec=10s
-TimeoutStartSec=120
-TimeoutStopSec=15
-StartLimitInterval=30min
-StartLimitBurst=5
-
-[Install]
-WantedBy=ceph-{fsid}.target
-""".format(
-    container_path=container_path,
-    install_path=install_path,
-    fsid=fsid,
-    uid=uid,
-    gid=gid,
-    data_dir=args.data_dir)
-    return u
-
-##################################
-
-class CephContainer:
-    def __init__(self,
-                 image,
-                 entrypoint,
-                 args=[],
-                 volume_mounts={},
-                 cname='',
-                 container_args=[]):
-        # type: (str, str, List[str], Dict[str, str], str, List[str]) -> None
-        self.image = image
-        self.entrypoint = entrypoint
-        self.args = args
-        self.volume_mounts = volume_mounts
-        self.cname = cname
-        self.container_args = container_args
-
-    def run_cmd(self):
-        # type: () -> List[str]
-        vols = [] # type: List[str]
-        envs = [] # type: List[str]
-        cname = [] # type: List[str]
-        vols = sum(
-            [['-v', '%s:%s' % (host_dir, container_dir)]
-             for host_dir, container_dir in self.volume_mounts.items()], [])
-        envs = [
-            '-e', 'CONTAINER_IMAGE=%s' % self.image,
-            '-e', 'NODE_NAME=%s' % get_hostname(),
-        ]
-        cname = ['--name', self.cname] if self.cname else []
-        return [
-            str(container_path),
-            'run',
-            '--rm',
-            '--net=host',
-        ] + self.container_args + \
-        cname + envs + \
-        vols + \
-        [
-            '--entrypoint', self.entrypoint,
-            self.image
-        ] + self.args # type: ignore
-
-    def shell_cmd(self, cmd):
-        # type: (List[str]) -> List[str]
-        vols = [] # type: List[str]
-        vols = sum(
-            [['-v', '%s:%s' % (host_dir, container_dir)]
-             for host_dir, container_dir in self.volume_mounts.items()], [])
-        envs = [
-            '-e', 'CONTAINER_IMAGE=%s' % self.image,
-            '-e', 'NODE_NAME=%s' % get_hostname(),
-        ]
-        cmd_args = [] # type: List[str]
-        if cmd:
-            cmd_args = ['-c'] + cmd
-        return [
-            str(container_path),
-            'run',
-            '--net=host',
-        ] + self.container_args + envs + vols + [
-            '--entrypoint', cmd[0],
-            self.image
-        ] + cmd[1:]
-
-    def exec_cmd(self, cmd):
-        # type: (List[str]) -> List[str]
-        return [
-            str(container_path),
-            'exec',
-        ] + self.container_args + [
-            self.cname,
-        ] + cmd
-
-    def run(self):
-        # type: () -> str
-        logger.debug(self.run_cmd())
-        out, _, _ = call_throws(self.run_cmd(), desc=self.entrypoint)
-        return out
-
-
-##################################
-
-def command_version():
-    # type: () -> int
-    out = CephContainer(args.image, 'ceph', ['--version']).run()
-    print(out.strip())
-    return 0
-
-##################################
-
-def command_pull():
-    # type: () -> None
-    logger.info('Pulling latest %s...' % args.image)
-    call_throws([container_path, 'pull', args.image])
-    out, err, ret = call_throws([
-        container_path, 'inspect',
-        '--format', '{{.Id}}',
-        args.image])
-    print(out.strip())
-
-##################################
-
-def command_bootstrap():
-    # type: () -> int
-
-    # verify output files
-    if not args.allow_overwrite:
-        for f in [args.output_config, args.output_keyring, args.output_pub_ssh_key]:
-            if os.path.exists(f):
-                raise Error('%s already exists; delete or pass '
-                              '--allow-overwrite to overwrite' % f)
-
-    # initial vars
-    fsid = args.fsid or make_fsid()
-    hostname = get_hostname()
-    mon_id = args.mon_id or hostname
-    mgr_id = args.mgr_id or generate_service_id()
-    logging.info('Cluster fsid: %s' % fsid)
-
-    # config
-    cp = read_config(args.config)
-    if args.mon_ip:
-        addr_arg = '[v2:%s:3300,v1:%s:6789]' % (args.mon_ip, args.mon_ip)
-        mon_ip = args.mon_ip
-    elif args.mon_addrv:
-        addr_arg = args.mon_addrv
-        mon_ip = args.mon_addrv.split(':')[1]
-    else:
-        raise Error('must specify --mon-ip or --mon-addrv')
-    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', 'container_image', args.image)
-    cpf = StringIO()
-    cp.write(cpf)
-    config = cpf.getvalue()
-
-    if not args.skip_ping_check:
-        logger.info('Verifying we can ping mon IP %s...' % mon_ip)
-        _, _, ret = call(['timeout', '5', 'ping', mon_ip, '-c', '1'], 'ping')
-        if ret:
-            raise Error('failed to ping %s' % mon_ip)
-
-    if not args.skip_pull:
-        logger.info('Pulling latest %s container...' % args.image)
-        call_throws([container_path, 'pull', args.image])
-
-    logger.info('Extracting ceph user uid/gid from container image...')
-    (uid, gid) = extract_uid_gid()
-
-    # create some initial keys
-    logger.info('Creating initial keys...')
-    mon_key = CephContainer(
-        image=args.image,
-        entrypoint='/usr/bin/ceph-authtool',
-        args=['--gen-print-key'],
-    ).run().strip()
-    admin_key = CephContainer(
-        image=args.image,
-        entrypoint='/usr/bin/ceph-authtool',
-        args=['--gen-print-key'],
-    ).run().strip()
-    mgr_key = CephContainer(
-        image=args.image,
-        entrypoint='/usr/bin/ceph-authtool',
-        args=['--gen-print-key'],
-    ).run().strip()
-    crash_key = CephContainer(
-        image=args.image,
-        entrypoint='/usr/bin/ceph-authtool',
-        args=['--gen-print-key'],
-    ).run().strip()
-
-    keyring = ('[mon.]\n'
-               '\tkey = %s\n'
-               '\tcaps mon = allow *\n'
-               '[client.admin]\n'
-               '\tkey = %s\n'
-               '\tcaps mon = allow *\n'
-               '\tcaps mds = allow *\n'
-               '\tcaps mgr = allow *\n'
-               '\tcaps osd = allow *\n'
-               '[mgr.%s]\n'
-               '\tkey = %s\n'
-               '\tcaps mon = profile mgr\n'
-               '\tcaps mds = allow *\n'
-               '\tcaps osd = allow *\n'
-               '[client.crash.%s]\n'
-               '\tkey = %s\n'
-               '\tcaps mon = profile crash\n'
-               '\tcaps mgr = profile crash\n'
-               % (mon_key, admin_key, mgr_id, mgr_key, hostname, crash_key))
-
-    # tmp keyring file
-    tmp_bootstrap_keyring = write_tmp(keyring, uid, gid)
-
-    # create initial monmap, tmp monmap file
-    logger.info('Creating initial monmap...')
-    tmp_monmap = write_tmp('', 0, 0)
-    out = CephContainer(
-        image=args.image,
-        entrypoint='/usr/bin/monmaptool',
-        args=['--create',
-              '--clobber',
-              '--fsid', fsid,
-              '--addv', mon_id, addr_arg,
-              '/tmp/monmap'
-        ],
-        volume_mounts={
-            tmp_monmap.name: '/tmp/monmap:z',
-        },
-    ).run()
-
-    # pass monmap file to ceph user for use by ceph-mon --mkfs below
-    os.fchown(tmp_monmap.fileno(), uid, gid)
-
-    # create mon
-    logger.info('Creating mon...')
-    create_daemon_dirs(fsid, 'mon', mon_id, uid, gid)
-    mon_dir = get_data_dir(fsid, 'mon', mon_id)
-    log_dir = get_log_dir(fsid)
-    out = CephContainer(
-        image=args.image,
-        entrypoint='/usr/bin/ceph-mon',
-        args=['--mkfs',
-              '-i', mon_id,
-              '--fsid', fsid,
-              '-c', '/dev/null',
-              '--monmap', '/tmp/monmap',
-              '--keyring', '/tmp/keyring',
-        ] + get_daemon_args(fsid, 'mon', mon_id),
-        volume_mounts={
-            log_dir: '/var/log/ceph:z',
-            mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % (mon_id),
-            tmp_bootstrap_keyring.name: '/tmp/keyring:z',
-            tmp_monmap.name: '/tmp/monmap:z',
-        },
-    ).run()
-
-    with open(mon_dir + '/config', 'w') as f:
-        os.fchown(f.fileno(), uid, gid)
-        os.fchmod(f.fileno(), 0o600)
-        f.write(config)
-
-    mon_c = get_container(fsid, 'mon', mon_id)
-    deploy_daemon(fsid, 'mon', mon_id, mon_c, uid, gid,
-                  config=None, keyring=None)
-
-    # client.admin key + config to issue various CLI commands
-    tmp_admin_keyring = write_tmp('[client.admin]\n'
-                                  '\tkey = ' + admin_key + '\n',
-                                       uid, gid)
-    tmp_config = write_tmp(config, uid, gid)
-
-    # a CLI helper to reduce our typing
-    def cli(cmd, extra_mounts={}):
-        # type: (List[str], Dict[str, str]) -> str
-        mounts = {
-            log_dir: '/var/log/ceph:z',
-            tmp_admin_keyring.name: '/etc/ceph/ceph.client.admin.keyring:z',
-            tmp_config.name: '/etc/ceph/ceph.conf:z',
-        }
-        for k, v in extra_mounts.items():
-            mounts[k] = v
-        return CephContainer(
-            image=args.image,
-            entrypoint='/usr/bin/ceph',
-            args=cmd,
-            volume_mounts=mounts,
-        ).run()
-
-    logger.info('Waiting for mon to start...')
-    while True:
-        c = CephContainer(
-            image=args.image,
-            entrypoint='/usr/bin/ceph',
-            args=[
-                'status'],
-            volume_mounts={
-                mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % (mon_id),
-                tmp_admin_keyring.name: '/etc/ceph/ceph.client.admin.keyring:z',
-                tmp_config.name: '/etc/ceph/ceph.conf:z',
-            },
-        )
-        out, err, ret = call(c.run_cmd(), c.entrypoint)
-        if ret == 0:
-            break
-        logger.info('mon is still not available yet, waiting...')
-        time.sleep(1)
-
-    # assimilate and minimize config
-    if not args.no_minimize_config:
-        logger.info('Assimilating anything we can from ceph.conf...')
-        cli([
-            'config', 'assimilate-conf',
-            '-i', '/var/lib/ceph/mon/ceph-%s/config' % mon_id
-        ], {
-            mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % mon_id
-        })
-        logger.info('Generating new minimal ceph.conf...')
-        cli([
-            'config', 'generate-minimal-conf',
-            '-o', '/var/lib/ceph/mon/ceph-%s/config' % mon_id
-        ], {
-            mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % mon_id
-        })
-        # re-read our minimized config
-        with open(mon_dir + '/config', 'r') as f:
-            config = f.read()
-        logger.info('Restarting the monitor...')
-        call_throws([
-            'systemctl',
-            'restart',
-            get_unit_name(fsid, 'mon', mon_id)
-        ])
-
-    # create mgr
-    logger.info('Creating mgr...')
-    mgr_keyring = '[mgr.%s]\n\tkey = %s\n' % (mgr_id, mgr_key)
-    mgr_c = get_container(fsid, 'mgr', mgr_id)
-    deploy_daemon(fsid, 'mgr', mgr_id, mgr_c, uid, gid, config, mgr_keyring)
-
-    # crash unit
-    logger.info('Creating crash agent...')
-    deploy_crash(fsid, uid, gid, config,
-                 '[client.crash.%s]\n\tkey = %s\n' % (hostname, crash_key))
-
-    # output files
-    with open(args.output_keyring, 'w') as f:
-        os.fchmod(f.fileno(), 0o600)
-        f.write('[client.admin]\n'
-                '\tkey = ' + admin_key + '\n')
-    logger.info('Wrote keyring to %s' % args.output_keyring)
-
-    with open(args.output_config, 'w') as f:
-        f.write(config)
-    logger.info('Wrote config to %s' % args.output_config)
-
-    logger.info('Waiting for mgr to start...')
-    while True:
-        out = cli(['status', '-f', 'json-pretty'])
-        j = json.loads(out)
-        if j.get('mgrmap', {}).get('available', False):
-            break
-        logger.info('mgr is still not available yet, waiting...')
-        time.sleep(1)
-
-    # ssh
-    if not args.skip_ssh:
-        logger.info('Enabling ssh module...')
-        cli(['mgr', 'module', 'enable', 'ssh'])
-        logger.info('Setting orchestrator backend to ssh...')
-        cli(['orchestrator', 'set', 'backend', 'ssh'])
-
-        logger.info('Generating ssh key...')
-        cli(['ssh', 'generate-key'])
-        ssh_pub = cli(['ssh', 'get-pub-key'])
-
-        with open(args.output_pub_ssh_key, 'w') as f:
-            f.write(ssh_pub)
-        logger.info('Wrote public SSH key to to %s' % args.output_pub_ssh_key)
-
-        logger.info('Adding key to root@localhost\'s authorized_keys...')
-        if not os.path.exists('/root/.ssh'):
-            os.mkdir('/root/.ssh', 0o700)
-        auth_keys_file = '/root/.ssh/authorized_keys'
-        add_newline = False
-        if os.path.exists(auth_keys_file):
-            with open(auth_keys_file, 'r') as f:
-                f.seek(0, os.SEEK_END)
-                if f.tell() > 0:
-                    f.seek(f.tell()-1, os.SEEK_SET) # go to last char
-                    if f.read() != '\n':
-                        add_newline = True
-        with open(auth_keys_file, 'a') as f:
-            os.fchmod(f.fileno(), 0o600)  # just in case we created it
-            if add_newline:
-                f.write('\n')
-            f.write(ssh_pub.strip() + '\n')
-
-        host = get_hostname()
-        logger.info('Adding host %s...' % host)
-        cli(['orchestrator', 'host', 'add', host])
-
-    if not args.skip_dashboard:
-        logger.info('Enabling the dashboard module...')
-        cli(['mgr', 'module', 'enable', 'dashboard'])
-        logger.info('Waiting for the module to be available...')
-        # FIXME: potential for an endless loop?
-        while True:
-            c_out = cli(['-h'])
-            if 'dashboard' in c_out:
-                break
-            logger.info('Dashboard not yet available, waiting...')
-            time.sleep(1)
-        logger.info('Generating a dashboard self-signed certificate...')
-        cli(['dashboard', 'create-self-signed-cert'])
-        logger.info('Creating initial admin user...')
-        password = args.initial_dashboard_password or generate_password()
-        cli(['dashboard', 'ac-user-create',
-             args.initial_dashboard_user, password,
-             'administrator'])
-        logger.info('Fetching dashboard port number...')
-        out = cli(['config', 'get', 'mgr', 'mgr/dashboard/ssl_server_port'])
-        port = int(out)
-        logger.info('Ceph Dashboard is now available at:\n\n'
-                    '\t     URL: https://%s:%s/\n'
-                    '\t    User: %s\n'
-                    '\tPassword: %s\n' % (
-                        get_fqdn(), port,
-                        args.initial_dashboard_user,
-                        password))
-
-    logger.info('You can access the Ceph CLI with:\n\n'
-                '\tsudo %s shell --fsid %s -c %s -k %s\n' % (
-                    sys.argv[0],
-                    fsid,
-                    args.output_config,
-                    args.output_keyring))
-
-    logger.info('Bootstrap complete.')
-    return 0
-
-##################################
-
-def command_deploy():
-    # type: () -> None
-    (daemon_type, daemon_id) = args.name.split('.', 1)
-    if daemon_type not in ['mon', 'mgr', 'mds', 'osd', 'rgw', 'rbd-mirror']:
-        raise Error('daemon type %s not recognized' % daemon_type)
-    (config, keyring, crash_keyring) = get_config_and_both_keyrings()
-    if daemon_type == 'mon':
-        if args.mon_ip:
-            config += '[mon.%s]\n\tpublic_addr = %s\n' % (daemon_id, args.mon_ip)
-        elif args.mon_addrv:
-            config += '[mon.%s]\n\tpublic_addrv = %s\n' % (daemon_id,
-                                                           args.mon_addrv)
-        elif args.mon_network:
-            config += '[mon.%s]\n\tpublic_network = %s\n' % (daemon_id,
-                                                             args.mon_network)
-        else:
-            raise Error('must specify --mon-ip or --mon-network')
-    (uid, gid) = extract_uid_gid()
-    c = get_container(args.fsid, daemon_type, daemon_id)
-    deploy_daemon(args.fsid, daemon_type, daemon_id, c, uid, gid,
-                  config, keyring)
-    if crash_keyring:
-        deploy_crash(args.fsid, uid, gid, config, crash_keyring)
-
-##################################
-
-def command_run():
-    # type: () -> int
-    (daemon_type, daemon_id) = args.name.split('.', 1)
-    c = get_container(args.fsid, daemon_type, daemon_id)
-    command = c.run_cmd()
-    logger.debug("Running command: %s" % ' '.join(command))
-    return subprocess.call(command)
-
-##################################
-
-@infer_fsid
-def command_shell():
-    # type: () -> int
-    if args.fsid:
-        make_log_dir(args.fsid)
-    if args.name:
-        if '.' in args.name:
-            (daemon_type, daemon_id) = args.name.split('.', 1)
-        else:
-            daemon_type = args.name
-            daemon_id = None
-    else:
-        daemon_type = 'osd'  # get the most mounts
-        daemon_id = None
-    mounts = get_container_mounts(args.fsid, daemon_type, daemon_id)
-    if args.config:
-        mounts[pathify(args.config)] = '/etc/ceph/ceph.conf:z'
-    if args.keyring:
-        mounts[pathify(args.keyring)] = '/etc/ceph/ceph.keyring:z'
-    container_args = ['--privileged']
-    if args.command:
-        command = args.command
-    else:
-        command = ['bash']
-        container_args += [
-            '-it',
-            '-e', 'LANG=C',
-            '-e', "PS1=%s" % CUSTOM_PS1,
-        ]
-    c = CephContainer(
-        image=args.image,
-        entrypoint='doesnotmatter',
-        args=[],
-        container_args=container_args,
-        volume_mounts=mounts)
-    command = c.shell_cmd(command)
-    logger.debug("Running command: %s" % ' '.join(command))
-    return subprocess.call(command)
-
-##################################
-
-@infer_fsid
-def command_enter():
-    # type: () -> int
-    if not args.fsid:
-        raise Error('must pass --fsid to specify cluster')
-    (daemon_type, daemon_id) = args.name.split('.', 1)
-    container_args = [] # type: List[str]
-    if args.command:
-        command = args.command
-    else:
-        command = ['bash']
-        container_args += [
-            '-it',
-            '-e', 'LANG=C',
-            '-e', "PS1=%s" % CUSTOM_PS1,
-        ]
-    c = get_container(args.fsid, daemon_type, daemon_id,
-                      container_args=container_args)
-    command = c.exec_cmd(command)
-    logger.debug("Running command: %s" % ' '.join(command))
-    return subprocess.call(command)
-
-##################################
-
-@infer_fsid
-def command_ceph_volume():
-    # type: () -> None
-    if args.fsid:
-        make_log_dir(args.fsid)
-
-    (uid, gid) = (0, 0) # ceph-volume runs as root
-    mounts = get_container_mounts(args.fsid, 'osd', None)
-
-    tmp_config = None
-    tmp_keyring = None
-
-    if args.config_and_keyring:
-        # note: this will always pull from args.config_and_keyring (we
-        # require it) and never args.config or args.keyring.
-        (config, keyring) = get_config_and_keyring()
-
-        # tmp keyring file
-        tmp_keyring = write_tmp(keyring, uid, gid)
-
-        # tmp config file
-        tmp_config = write_tmp(config, uid, gid)
-
-        mounts[tmp_config.name] = '/etc/ceph/ceph.conf:z'
-        mounts[tmp_keyring.name] = '/var/lib/ceph/bootstrap-osd/ceph.keyring:z'
-
-    c = CephContainer(
-        image=args.image,
-        entrypoint='/usr/sbin/ceph-volume',
-        args=args.command,
-        container_args=['--privileged'],
-        volume_mounts=mounts,
-    )
-    out, err, code = call_throws(c.run_cmd(), verbose=True)
-    if not code:
-        print(out)
-
-##################################
-
-@infer_fsid
-def command_unit():
-    # type: () -> None
-    if not args.fsid:
-        raise Error('must pass --fsid to specify cluster')
-    (daemon_type, daemon_id) = args.name.split('.', 1)
-    unit_name = get_unit_name(args.fsid, daemon_type, daemon_id)
-    call_throws([
-        'systemctl',
-        args.command,
-        unit_name])
-
-##################################
-
-@infer_fsid
-def command_logs():
-    # type: () -> None
-    if not args.fsid:
-        raise Error('must pass --fsid to specify cluster')
-    cmd = [str(container_path), 'logs'] # type: List[str]
-    if args.follow:
-        cmd.append('-f')
-    if args.tail:
-        cmd.append('--tail=' + str(args.tail))
-    cmd.append('ceph-%s-%s' % (args.fsid, args.name))
-
-    # call this directly, without our wrapper, so that we get an unmolested
-    # stdout with logger prefixing.
-    logger.debug("Running command: %s" % ' '.join(cmd))
-    subprocess.call(cmd) # type: ignore
-
-##################################
-
-def command_ls():
-    # type: () -> None
-    ls = list_daemons(detail=not args.no_detail,
-                      legacy_dir=args.legacy_dir)
-    print(json.dumps(ls, indent=4))
-
-def list_daemons(detail=True, legacy_dir=None):
-    # type: (bool, Optional[str]) -> List[Dict[str, str]]
-    host_version = None
-    ls = []
-
-    data_dir = args.data_dir
-    if legacy_dir is not None:
-        data_dir = os.path.abspath(legacy_dir + data_dir)
-
-    # /var/lib/ceph
-    if os.path.exists(data_dir):
-        for i in os.listdir(data_dir):
-            if i in ['mon', 'osd', 'mds', 'mgr']:
-                daemon_type = i
-                for j in os.listdir(os.path.join(data_dir, i)):
-                    if '-' not in j:
-                        continue
-                    (cluster, daemon_id) = j.split('-', 1)
-                    fsid = get_legacy_daemon_fsid(
-                            cluster, daemon_type, daemon_id,
-                            legacy_dir=legacy_dir)
-                    i = {
-                        'style': 'legacy',
-                        'name': '%s.%s' % (daemon_type, daemon_id),
-                        'fsid': fsid if fsid is not None else 'unknown',
-                    }
-                    if detail:
-                        (i['enabled'], i['state']) = check_unit(
-                            'ceph-%s@%s' % (daemon_type, daemon_id))
-                        if not host_version:
-                            try:
-                                out, err, code = call(['ceph', '-v'])
-                                if not code and out.startswith('ceph version '):
-                                    host_version = out.split(' ')[2]
-                            except Exception:
-                                pass
-                        i['host_version'] = host_version
-                    ls.append(i)
-            elif is_fsid(i):
-                fsid = i
-                for j in os.listdir(os.path.join(data_dir, i)):
-                    if j == 'crash':
-                        name = 'crash'
-                        unit_name = 'ceph-%s-crash.service' % fsid
-                    elif '.' in j:
-                        name = j
-                        (daemon_type, daemon_id) = j.split('.', 1)
-                        unit_name = get_unit_name(fsid,
-                                                  daemon_type,
-                                                  daemon_id)
-                    else:
-                        continue
-                    i = {
-                        'style': 'ceph-daemon:v1',
-                        'name': name,
-                        'fsid': fsid,
-                    }
-                    if detail:
-                        # get container id
-                        (i['enabled'], i['state']) = check_unit(unit_name)
-                        container_id = None
-                        image_name = None
-                        image_id = None
-                        version = None
-                        out, err, code = call(
-                            [
-                                container_path, 'inspect',
-                                '--format', '{{.Id}},{{.Config.Image}},{{.Image}}',
-                                'ceph-%s-%s' % (fsid, j)
-                            ],
-                            verbose_on_failure=False)
-                        if not code:
-                            (container_id, image_name, image_id) = out.strip().split(',')
-                            out, err, code = call(
-                                [container_path, 'exec', container_id,
-                                 'ceph', '-v'])
-                            if not code and out.startswith('ceph version '):
-                                version = out.split(' ')[2]
-                        i['container_id'] = container_id
-                        i['container_image_name'] = image_name
-                        i['container_image_id'] = image_id
-                        i['version'] = version
-                    ls.append(i)
-
-    # /var/lib/rook
-    # WRITE ME
-    return ls
-
-
-##################################
-
-def command_adopt():
-    # type: () -> None
-    (daemon_type, daemon_id) = args.name.split('.', 1)
-    (uid, gid) = extract_uid_gid()
-    if args.style == 'legacy':
-        fsid = get_legacy_daemon_fsid(args.cluster,
-                                      daemon_type,
-                                      daemon_id,
-                                      legacy_dir=args.legacy_dir)
-        if not fsid:
-            raise Error('could not detect legacy fsid; set fsid in ceph.conf')
-
-        # NOTE: implicit assumption here that the units correspond to the
-        # cluster we are adopting based on the /etc/{defaults,sysconfig}/ceph
-        # CLUSTER field.
-        unit_name = 'ceph-%s@%s' % (daemon_type, daemon_id)
-        (enabled, state) = check_unit(unit_name)
-
-        if state == 'running':
-            logger.info('Stopping old systemd unit %s...' % unit_name)
-            call_throws(['systemctl', 'stop', unit_name])
-        if enabled:
-            logger.info('Disabling old systemd unit %s...' % unit_name)
-            call_throws(['systemctl', 'disable', unit_name])
-
-        # data
-        logger.info('Moving data...')
-        data_dir_src = ('/var/lib/ceph/%s/%s-%s' %
-                        (daemon_type, args.cluster, daemon_id))
-        data_dir_src = os.path.abspath(args.legacy_dir + data_dir_src)
-        data_dir_dst = make_data_dir(fsid, daemon_type, daemon_id,
-                                     uid=uid, gid=gid)
-        move_files(glob(os.path.join(data_dir_src, '*')),
-                   data_dir_dst,
-                   uid=uid, gid=gid)
-        logger.debug('Remove dir \'%s\'' % (data_dir_src))
-        if os.path.ismount(data_dir_src):
-            call_throws(['umount', data_dir_src])
-        os.rmdir(data_dir_src)
-
-        # config
-        config_src = '/etc/ceph/%s.conf' % (args.cluster)
-        config_src = os.path.abspath(args.legacy_dir + config_src)
-        config_dst = os.path.join(data_dir_dst, 'config')
-        copy_files([config_src], config_dst, uid=uid, gid=gid)
-
-        # logs
-        logger.info('Moving logs...')
-        log_dir_src = ('/var/log/ceph/%s-%s.%s.log*' %
-                        (args.cluster, daemon_type, daemon_id))
-        log_dir_src = os.path.abspath(args.legacy_dir + log_dir_src)
-        log_dir_dst = make_log_dir(fsid, uid=uid, gid=gid)
-        move_files(glob(log_dir_src),
-                   log_dir_dst,
-                   uid=uid, gid=gid)
-
-        logger.info('Creating new units...')
-        c = get_container(fsid, daemon_type, daemon_id)
-        deploy_daemon_units(fsid, uid, gid, daemon_type, daemon_id, c,
-                            enable=True,  # unconditionally enable the new unit
-                            start=(state == 'running'))
-        update_firewalld(daemon_type)
-
-    else:
-        raise Error('adoption of style %s not implemented' % args.style)
-
-##################################
-
-def command_rm_daemon():
-    # type: () -> None
-    (daemon_type, daemon_id) = args.name.split('.', 1)
-    if daemon_type in ['mon', 'osd'] and not args.force:
-        raise Error('must pass --force to proceed: '
-                      'this command may destroy precious data!')
-    unit_name = get_unit_name(args.fsid, daemon_type, daemon_id)
-    call(['systemctl', 'stop', unit_name],
-         verbose_on_failure=False)
-    call(['systemctl', 'reset-failed', unit_name],
-         verbose_on_failure=False)
-    call(['systemctl', 'disable', unit_name],
-         verbose_on_failure=False)
-    data_dir = get_data_dir(args.fsid, daemon_type, daemon_id)
-    call_throws(['rm', '-rf', data_dir])
-
-##################################
-
-def command_rm_cluster():
-    # type: () -> None
-    if not args.force:
-        raise Error('must pass --force to proceed: '
-                      'this command may destroy precious data!')
-
-    # stop + disable individual daemon units
-    for d in list_daemons(detail=False):
-        if d['fsid'] != args.fsid:
-            continue
-        if d['style'] != 'ceph-daemon:v1':
-            continue
-        unit_name = get_unit_name(args.fsid, d['name'])
-        call(['systemctl', 'stop', unit_name],
-             verbose_on_failure=False)
-        call(['systemctl', 'reset-failed', unit_name],
-             verbose_on_failure=False)
-        call(['systemctl', 'disable', unit_name],
-             verbose_on_failure=False)
-
-    # cluster units
-    for unit_name in ['ceph-%s.target' % args.fsid,
-                      'ceph-%s-crash.service' % args.fsid]:
-        call(['systemctl', 'stop', unit_name],
-             verbose_on_failure=False)
-        call(['systemctl', 'reset-failed', unit_name],
-             verbose_on_failure=False)
-        call(['systemctl', 'disable', unit_name],
-             verbose_on_failure=False)
-
-    slice_name = 'system-%s.slice' % (('ceph-%s' % args.fsid).replace('-',
-                                                                      '\\x2d'))
-    call(['systemctl', 'stop', slice_name],
-         verbose_on_failure=False)
-
-    # rm units
-    call_throws(['rm', '-f', args.unit_dir +
-                             '/ceph-%s@.service' % args.fsid])
-    call_throws(['rm', '-f', args.unit_dir +
-                             '/ceph-%s-crash.service' % args.fsid])
-    call_throws(['rm', '-f', args.unit_dir +
-                             '/ceph-%s.target' % args.fsid])
-    call_throws(['rm', '-rf',
-                  args.unit_dir + '/ceph-%s.target.wants' % args.fsid])
-    # rm data
-    call_throws(['rm', '-rf', args.data_dir + '/' + args.fsid])
-    # rm logs
-    call_throws(['rm', '-rf', args.log_dir + '/' + args.fsid])
-    call_throws(['rm', '-rf', args.log_dir +
-                             '/*.wants/ceph-%s@*' % args.fsid])
-    # rm logrotate config
-    call_throws(['rm', '-f', args.logrotate_dir + '/ceph-%s' % args.fsid])
-
-
-##################################
-
-def check_time_sync():
-    units = ['chronyd.service', 'systemd-timesyncd.service', 'ntpd.service']
-    for u in units:
-        (enabled, state) = check_unit(u)
-        if enabled and state == 'running':
-            logger.info('Time sync unit %s is enabled and running' % u)
-            return True
-    logger.warning('No time sync service is running; checked for %s' % units)
-    return False
-
-def command_check_host():
-    # caller already checked for docker/podman
-    logger.info('podman|docker (%s) is present' % container_path)
-
-    if not find_program('systemctl'):
-        raise RuntimeError('unable to location systemctl')
-    logger.info('systemctl is present')
-
-    if not find_program('lvcreate'):
-        raise RuntimeError('LVM does not appear to be installed')
-    logger.info('LVM2 is present')
-
-    # check for configured+running chronyd or ntp
-    if not check_time_sync():
-        raise RuntimeError('No time synchronization is active (checked all of %s)' %
-                           units)
-
-    logger.info('Host looks OK')
-
-
-##################################
-
-def _get_parser():
-    # type: () -> argparse.ArgumentParser
-    parser = argparse.ArgumentParser(
-        description='Bootstrap Ceph daemons with systemd and containers.',
-        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
-    parser.add_argument(
-        '--image',
-        default=os.environ.get('CEPH_DAEMON_IMAGE', DEFAULT_IMAGE),
-        help='container image. Can also be set via the "CEPH_DAEMON_IMAGE" '
-        'env var')
-    parser.add_argument(
-        '--docker',
-        action='store_true',
-        help='use docker instead of podman')
-    parser.add_argument(
-        '--data-dir',
-        default=DATA_DIR,
-        help='base directory for daemon data')
-    parser.add_argument(
-        '--log-dir',
-        default=LOG_DIR,
-        help='base directory for daemon logs')
-    parser.add_argument(
-        '--logrotate-dir',
-        default=LOGROTATE_DIR,
-        help='location of logrotate configuration files')
-    parser.add_argument(
-        '--unit-dir',
-        default=UNIT_DIR,
-        help='base directory for systemd units')
-    parser.add_argument(
-        '--verbose', '-v',
-        action='store_true',
-        help='Show debug-level log messages')
-    subparsers = parser.add_subparsers(help='sub-command')
-
-    parser_version = subparsers.add_parser(
-        'version', help='get ceph version from container')
-    parser_version.set_defaults(func=command_version)
-
-    parser_pull = subparsers.add_parser(
-        'pull', help='pull latest image version')
-    parser_pull.set_defaults(func=command_pull)
-
-    parser_ls = subparsers.add_parser(
-        'ls', help='list daemon instances on this host')
-    parser_ls.set_defaults(func=command_ls)
-    parser_ls.add_argument(
-        '--no-detail',
-        action='store_true',
-        help='Do not include daemon status')
-    parser_ls.add_argument(
-        '--legacy-dir',
-        default='/',
-        help='base directory for legacy daemon data')
-
-    parser_adopt = subparsers.add_parser(
-        'adopt', help='adopt daemon deployed with a different tool')
-    parser_adopt.set_defaults(func=command_adopt)
-    parser_adopt.add_argument(
-        '--name', '-n',
-        required=True,
-        help='daemon name (type.id)')
-    parser_adopt.add_argument(
-        '--style',
-        required=True,
-        help='deployment style (legacy, ...)')
-    parser_adopt.add_argument(
-        '--cluster',
-        default='ceph',
-        help='cluster name')
-    parser_adopt.add_argument(
-        '--legacy-dir',
-        default='/',
-        help='base directory for legacy daemon data')
-    parser_adopt.add_argument(
-        '--skip-firewalld',
-        action='store_true',
-        help='Do not configure firewalld')
-
-    parser_rm_daemon = subparsers.add_parser(
-        'rm-daemon', help='remove daemon instance')
-    parser_rm_daemon.set_defaults(func=command_rm_daemon)
-    parser_rm_daemon.add_argument(
-        '--name', '-n',
-        required=True,
-        help='daemon name (type.id)')
-    parser_rm_daemon.add_argument(
-        '--fsid',
-        required=True,
-        help='cluster FSID')
-    parser_rm_daemon.add_argument(
-        '--force',
-        action='store_true',
-        help='proceed, even though this may destroy valuable data')
-
-    parser_rm_cluster = subparsers.add_parser(
-        'rm-cluster', help='remove all daemons for a cluster')
-    parser_rm_cluster.set_defaults(func=command_rm_cluster)
-    parser_rm_cluster.add_argument(
-        '--fsid',
-        required=True,
-        help='cluster FSID')
-    parser_rm_cluster.add_argument(
-        '--force',
-        action='store_true',
-        help='proceed, even though this may destroy valuable data')
-
-    parser_run = subparsers.add_parser(
-        'run', help='run a ceph daemon, in a container, in the foreground')
-    parser_run.set_defaults(func=command_run)
-    parser_run.add_argument(
-        '--name', '-n',
-        required=True,
-        help='daemon name (type.id)')
-    parser_run.add_argument(
-        '--fsid',
-        required=True,
-        help='cluster FSID')
-
-    parser_shell = subparsers.add_parser(
-        'shell', help='run an interactive shell inside a daemon container')
-    parser_shell.set_defaults(func=command_shell)
-    parser_shell.add_argument(
-        '--fsid',
-        help='cluster FSID')
-    parser_shell.add_argument(
-        '--name', '-n',
-        help='daemon name (type.id)')
-    parser_shell.add_argument(
-        '--config', '-c',
-        help='ceph.conf to pass through to the container')
-    parser_shell.add_argument(
-        '--keyring', '-k',
-        help='ceph.keyring to pass through to the container')
-    parser_shell.add_argument(
-        'command', nargs='*',
-        help='command (optional)')
-
-    parser_enter = subparsers.add_parser(
-        'enter', help='run an interactive shell inside a running daemon container')
-    parser_enter.set_defaults(func=command_enter)
-    parser_enter.add_argument(
-        '--fsid',
-        help='cluster FSID')
-    parser_enter.add_argument(
-        '--name', '-n',
-        required=True,
-        help='daemon name (type.id)')
-    parser_enter.add_argument(
-        'command', nargs='*',
-        help='command')
-
-    parser_ceph_volume = subparsers.add_parser(
-        'ceph-volume', help='run ceph-volume inside a container')
-    parser_ceph_volume.set_defaults(func=command_ceph_volume)
-    parser_ceph_volume.add_argument(
-        '--fsid',
-        help='cluster FSID')
-    parser_ceph_volume.add_argument(
-        '--config-and-keyring',
-        help='JSON file with config and (client.bootrap-osd) key')
-    parser_ceph_volume.add_argument(
-        'command', nargs='+',
-        help='command')
-
-    parser_unit = subparsers.add_parser(
-        'unit', help='operate on the daemon\'s systemd unit')
-    parser_unit.set_defaults(func=command_unit)
-    parser_unit.add_argument(
-        'command',
-        help='systemd command (start, stop, restart, enable, disable, ...)')
-    parser_unit.add_argument(
-        '--fsid',
-        help='cluster FSID')
-    parser_unit.add_argument(
-        '--name', '-n',
-        required=True,
-        help='daemon name (type.id)')
-
-    parser_logs = subparsers.add_parser(
-        'logs', help='fetch the log for a daemon\'s container')
-    parser_logs.set_defaults(func=command_logs)
-    parser_logs.add_argument(
-        '--fsid',
-        help='cluster FSID')
-    parser_logs.add_argument(
-        '--name', '-n',
-        required=True,
-        help='daemon name (type.id)')
-    parser_logs.add_argument(
-        '-f', '--follow',
-        action='store_true',
-        help='Follow log output')
-    parser_logs.add_argument(
-        '--tail',
-        help='Output the specified number of lines at the end of the log')
-
-    parser_bootstrap = subparsers.add_parser(
-        'bootstrap', help='bootstrap a cluster (mon + mgr daemons)')
-    parser_bootstrap.set_defaults(func=command_bootstrap)
-    parser_bootstrap.add_argument(
-        '--config', '-c',
-        help='ceph conf file to incorporate')
-    parser_bootstrap.add_argument(
-        '--mon-id',
-        required=False,
-        help='mon id (default: local hostname)')
-    parser_bootstrap.add_argument(
-        '--mon-addrv',
-        help='mon IPs (e.g., [v2:localipaddr:3300,v1:localipaddr:6789])')
-    parser_bootstrap.add_argument(
-        '--mon-ip',
-        help='mon IP')
-    parser_bootstrap.add_argument(
-        '--mgr-id',
-        required=False,
-        help='mgr id (default: local hostname)')
-    parser_bootstrap.add_argument(
-        '--fsid',
-        help='cluster FSID')
-    parser_bootstrap.add_argument(
-        '--output-keyring',
-        default='ceph.client.admin.keyring',
-        help='location to write keyring file with new cluster admin and mon keys')
-    parser_bootstrap.add_argument(
-        '--output-config',
-        default='ceph.conf',
-        help='location to write conf file to connect to new cluster')
-    parser_bootstrap.add_argument(
-        '--output-pub-ssh-key',
-        default='ceph.pub',
-        help='location to write the cluster\'s public SSH key')
-    parser_bootstrap.add_argument(
-        '--skip-ssh',
-        action='store_true',
-        help='skip setup of ssh key on local host')
-    parser_bootstrap.add_argument(
-        '--initial-dashboard-user',
-        default='admin',
-        help='Initial user for the dashboard')
-    parser_bootstrap.add_argument(
-        '--initial-dashboard-password',
-        help='Initial password for the initial dashboard user')
-    parser_bootstrap.add_argument(
-        '--skip-dashboard',
-        action='store_true',
-        help='do not enable the Ceph Dashboard')
-    parser_bootstrap.add_argument(
-        '--no-minimize-config',
-        action='store_true',
-        help='do not assimilate and minimize the config file')
-    parser_bootstrap.add_argument(
-        '--skip-ping-check',
-        action='store_true',
-        help='do not verify that mon IP is pingable')
-    parser_bootstrap.add_argument(
-        '--skip-pull',
-        action='store_true',
-        help='do not pull the latest image before bootstrapping')
-    parser_bootstrap.add_argument(
-        '--skip-firewalld',
-        action='store_true',
-        help='Do not configure firewalld')
-    parser_bootstrap.add_argument(
-        '--allow-overwrite',
-        action='store_true',
-        help='allow overwrite of existing --output-* config/keyring/ssh files')
-
-    parser_deploy = subparsers.add_parser(
-        'deploy', help='deploy a daemon')
-    parser_deploy.set_defaults(func=command_deploy)
-    parser_deploy.add_argument(
-        '--name',
-        required=True,
-        help='daemon name (type.id)')
-    parser_deploy.add_argument(
-        '--fsid',
-        required=True,
-        help='cluster FSID')
-    parser_deploy.add_argument(
-        '--config', '-c',
-        help='config file for new daemon')
-    parser_deploy.add_argument(
-        '--keyring',
-        help='keyring for new daemon')
-    parser_deploy.add_argument(
-        '--crash-keyring',
-        help='crash keyring for crash agent daemon')
-    parser_deploy.add_argument(
-        '--key',
-        help='key for new daemon')
-    parser_deploy.add_argument(
-        '--config-and-keyrings',
-        help='JSON file with config and keyrings for the daemon and crash agent')
-    parser_deploy.add_argument(
-        '--mon-ip',
-        help='mon IP')
-    parser_deploy.add_argument(
-        '--mon-addrv',
-        help='mon IPs (e.g., [v2:localipaddr:3300,v1:localipaddr:6789])')
-    parser_deploy.add_argument(
-        '--mon-network',
-        help='mon network (CIDR)')
-    parser_deploy.add_argument(
-        '--osd-fsid',
-        help='OSD uuid, if creating an OSD container')
-    parser_deploy.add_argument(
-        '--skip-firewalld',
-        action='store_true',
-        help='Do not configure firewalld')
-
-    parser_check_host = subparsers.add_parser(
-        'check-host', help='check host configuration')
-    parser_check_host.set_defaults(func=command_check_host)
-
-    return parser
-
-
-if __name__ == "__main__":
-    # allow argv to be injected
-    try:
-        av = injected_argv # type: ignore
-    except NameError:
-        av = sys.argv[1:]
-    parser = _get_parser()
-    args = parser.parse_args(av)
-
-    if args.verbose:
-        logging.basicConfig(level=logging.DEBUG)
-    else:
-        logging.basicConfig(level=logging.INFO)
-    logger = logging.getLogger('ceph-daemon')
-
-    # root?
-    if os.geteuid() != 0:
-        sys.stderr.write('ERROR: ceph-daemon should be run as root\n')
-        sys.exit(1)
-
-    # podman or docker?
-    if args.docker:
-        container_path = find_program('docker')
-    else:
-        for i in CONTAINER_PREFERENCE:
-            try:
-                container_path = find_program(i)
-                break
-            except Exception as e:
-                logger.debug('Could not locate %s: %s' % (i, e))
-        if not container_path:
-            sys.stderr.write('Unable to locate any of %s\n' % CONTAINER_PREFERENCE)
-            sys.exit(1)
-
-    if 'func' not in args:
-        sys.stderr.write('No command specified; pass -h or --help for usage\n')
-        sys.exit(1)
-    try:
-        r = args.func()
-    except Error as e:
-        if args.verbose:
-            raise
-        sys.stderr.write('ERROR: %s\n' % e)
-        sys.exit(1)
-    if not r:
-        r = 0
-    sys.exit(r)
diff --git a/src/ceph-daemon/mypy.ini b/src/ceph-daemon/mypy.ini
deleted file mode 100644 (file)
index 1215375..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-[mypy]
-ignore_missing_imports = True
\ No newline at end of file
diff --git a/src/ceph-daemon/tests/__init__.py b/src/ceph-daemon/tests/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/ceph-daemon/tests/test_ceph_daemon.py b/src/ceph-daemon/tests/test_ceph_daemon.py
deleted file mode 100644 (file)
index 8425c78..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-import mock
-import os
-import sys
-import unittest
-
-if sys.version_info >= (3, 3):
-    from importlib.machinery import SourceFileLoader
-    cd = SourceFileLoader('ceph-daemon', 'ceph-daemon').load_module()
-else:
-    import imp
-    cd = imp.load_source('ceph-daemon', 'ceph-daemon')
-
-class TestCephDaemon(unittest.TestCase):
-    def test_is_fsid(self):
-        self.assertFalse(cd.is_fsid('no-uuid'))
-
-    def test__get_parser_image(self):
-        p = cd._get_parser()
-        args = p.parse_args(['--image', 'foo', 'version'])
-        assert args.image == 'foo'
-
-    @mock.patch.dict(os.environ,{'CEPH_DAEMON_IMAGE':'bar'})
-    def test__get_parser_image_with_envvar(self):
-        p = cd._get_parser()
-        args = p.parse_args(['version'])
-        assert args.image == 'bar'
diff --git a/src/ceph-daemon/tox.ini b/src/ceph-daemon/tox.ini
deleted file mode 100644 (file)
index 7407aac..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-[tox]
-envlist = py27, py3, mypy
-
-[testenv]
-skipsdist=true
-skip_install=true
-deps =
-  pytest
-  mock
-commands=pytest {posargs}
-
-[testenv:mypy]
-basepython = python3
-deps = mypy
-commands = mypy {posargs:ceph-daemon}
\ No newline at end of file
diff --git a/src/cephadm/CMakeLists.txt b/src/cephadm/CMakeLists.txt
new file mode 100644 (file)
index 0000000..998776f
--- /dev/null
@@ -0,0 +1,4 @@
+if(WITH_TESTS)
+  include(AddCephTest)
+  add_tox_test(cephadm TOX_ENVS mypy)
+endif()
diff --git a/src/cephadm/cephadm b/src/cephadm/cephadm
new file mode 100755 (executable)
index 0000000..2a9b0e0
--- /dev/null
@@ -0,0 +1,2131 @@
+#!/usr/bin/python
+
+DEFAULT_IMAGE='ceph/daemon-base:latest-master-devel'  # FIXME when octopus is ready!!!
+DATA_DIR='/var/lib/ceph'
+LOG_DIR='/var/log/ceph'
+LOGROTATE_DIR='/etc/logrotate.d'
+UNIT_DIR='/etc/systemd/system'
+LOG_DIR_MODE=0o770
+DATA_DIR_MODE=0o700
+CONTAINER_PREFERENCE = ['podman', 'docker']  # prefer podman to docker
+CUSTOM_PS1=r'[ceph: \u@\h \W]\$ '
+
+"""
+You can invoke ceph-daemon in two ways:
+
+1. The normal way, at the command line.
+
+2. By piping the script to the python3 binary.  In this latter case, you should
+   prepend one or more lines to the beginning of the script.
+
+   For arguments,
+
+       injected_argv = [...]
+
+   e.g.,
+
+       injected_argv = ['ls']
+
+   For reading stdin from the '--config-and-json -' argument,
+
+       injected_stdin = '...'
+"""
+
+import argparse
+try:
+    from ConfigParser import ConfigParser   # py2
+except ImportError:
+    from configparser import ConfigParser   # py3
+import fcntl
+try:
+    from StringIO import StringIO           # py2
+except ImportError:
+    from io import StringIO                 # py3
+import json
+import logging
+import os
+import random
+import re
+import select
+import shutil
+import socket
+import string
+import subprocess
+import sys
+import tempfile
+import time
+try:
+    from typing import Dict, List, Tuple, Optional, Union
+except ImportError:
+    pass
+import uuid
+
+from distutils.spawn import find_executable
+from functools import wraps
+from glob import glob
+
+
+container_path = None
+
+class Error(Exception):
+    pass
+
+##################################
+# Popen wrappers, lifted from ceph-volume
+
+def call(command, desc=None, verbose=False, **kwargs):
+    """
+    Wrap subprocess.Popen to
+
+    - log stdout/stderr to a logger,
+    - decode utf-8
+    - cleanly return out, err, returncode
+
+    If verbose=True, log at info (instead of debug) level.
+
+    :param verbose_on_failure: On a non-zero exit status, it will forcefully set
+                               logging ON for the terminal
+    """
+    if not desc:
+        desc = command[0]
+    verbose_on_failure = kwargs.pop('verbose_on_failure', True)
+
+    logger.debug("Running command: %s" % ' '.join(command))
+    process = subprocess.Popen(
+        command,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        close_fds=True,
+        **kwargs
+    )
+    # get current p.stdout flags, add O_NONBLOCK
+    stdout_flags = fcntl.fcntl(process.stdout, fcntl.F_GETFL)
+    stderr_flags = fcntl.fcntl(process.stderr, fcntl.F_GETFL)
+    fcntl.fcntl(process.stdout, fcntl.F_SETFL, stdout_flags | os.O_NONBLOCK)
+    fcntl.fcntl(process.stderr, fcntl.F_SETFL, stderr_flags | os.O_NONBLOCK)
+
+    out = ''
+    err = ''
+    reads = None
+    stop = False
+    out_buffer = ''   # partial line (no newline yet)
+    err_buffer = ''   # partial line (no newline yet)
+    while not stop:
+        if reads and process.poll() is not None:
+            # we want to stop, but first read off anything remaining
+            # on stdout/stderr
+            stop = True
+        else:
+            reads, _, _ = select.select(
+                [process.stdout.fileno(), process.stderr.fileno()],
+                [], []
+            )
+        for fd in reads:
+            try:
+                message = os.read(fd, 1024)
+                if not isinstance(message, str):
+                    message = message.decode('utf-8')
+                if fd == process.stdout.fileno():
+                    out += message
+                    message = out_buffer + message
+                    lines = message.split('\n')
+                    out_buffer = lines.pop()
+                    for line in lines:
+                        if verbose:
+                            logger.info(desc + ':stdout ' + line)
+                        else:
+                            logger.debug(desc + ':stdout ' + line)
+                elif fd == process.stderr.fileno():
+                    err += message
+                    message = err_buffer + message
+                    lines = message.split('\n')
+                    err_buffer = lines.pop()
+                    for line in lines:
+                        if verbose:
+                            logger.info(desc + ':stderr ' + line)
+                        else:
+                            logger.debug(desc + ':stderr ' + line)
+                else:
+                    assert False
+            except (IOError, OSError):
+                pass
+
+    returncode = process.wait()
+
+    if out_buffer != '':
+        if verbose:
+            logger.info(desc + ':stdout ' + out_buffer)
+        else:
+            logger.debug(desc + ':stdout ' + out_buffer)
+    if err_buffer != '':
+        if verbose:
+            logger.info(desc + ':stderr ' + err_buffer)
+        else:
+            logger.debug(desc + ':stderr ' + err_buffer)
+
+    if returncode != 0 and verbose_on_failure and not verbose:
+        # dump stdout + stderr
+        logger.info('Non-zero exit code %d from %s' % (returncode, ' '.join(command)))
+        for line in out.splitlines():
+            logger.info(desc + ':stdout ' + line)
+        for line in err.splitlines():
+            logger.info(desc + ':stderr ' + line)
+
+    return out, err, returncode
+
+def call_throws(command, **kwargs):
+    out, err, ret = call(command, **kwargs)
+    if ret:
+        raise RuntimeError('Failed command: %s' % ' '.join(command))
+    return out, err, ret
+
+##################################
+
+def read_config(fn):
+    # type: (Optional[str]) -> ConfigParser
+    # bend over backwards here because py2's ConfigParser doesn't like
+    # whitespace before config option names (e.g., '\n foo = bar\n').
+    # Yeesh!
+    cp = ConfigParser()
+    if fn:
+        with open(fn, 'r') as f:
+            raw_conf = f.read()
+        nice_conf = re.sub('\n(\s)+', '\n', raw_conf)
+        cp.readfp(StringIO(nice_conf))
+    return cp
+
+def pathify(p):
+    # type: (str) -> str
+    if not p.startswith('/'):
+        return os.path.join(os.getcwd(), p)
+    return p
+
+def get_hostname():
+    # type: () -> str
+    return socket.gethostname()
+
+def get_fqdn():
+    # type: () -> str
+    return socket.getfqdn() or socket.gethostname()
+
+def generate_service_id():
+    # type: () -> str
+    return ''.join(random.choice(string.ascii_lowercase)
+                   for _ in range(6))
+
+def generate_password():
+    # type: () -> str
+    return ''.join(random.choice(string.ascii_lowercase + string.digits)
+                   for i in range(10))
+
+def make_fsid():
+    # type: () -> str
+    return str(uuid.uuid1())
+
+def is_fsid(s):
+    # type: (str) -> bool
+    try:
+        uuid.UUID(s)
+    except ValueError:
+        return False
+    return True
+
+def infer_fsid(func):
+    """
+    If we only find a single fsid in /var/lib/ceph/*, use that
+    """
+    @wraps(func)
+    def _infer_fsid():
+        if args.fsid:
+            logger.debug('Using specified fsid: %s' % args.fsid)
+            return func()
+
+        fsid_list = []
+        if os.path.exists(args.data_dir):
+            for i in os.listdir(args.data_dir):
+                if is_fsid(i):
+                    fsid_list.append(i)
+        logger.debug('Found fsids %s' % str(fsid_list))
+
+        if not fsid_list:
+            # TODO: raise?
+            return func()
+
+        if len(fsid_list) > 1:
+            raise Error('cannot infer fsid, must specify --fsid')
+
+        logger.info('Inferring fsid %s' % fsid_list[0])
+        args.fsid = fsid_list[0]
+        return func()
+    return _infer_fsid
+
+def write_tmp(s, uid, gid):
+    tmp_f = tempfile.NamedTemporaryFile(mode='w',
+                                        prefix='ceph-tmp')
+    os.fchown(tmp_f.fileno(), uid, gid)
+    tmp_f.write(s)
+    tmp_f.flush()
+
+    return tmp_f
+
+def makedirs(dir, uid, gid, mode):
+    # type: (str, int, int, int) -> None
+    if not os.path.exists(dir):
+        os.makedirs(dir, mode=mode)
+    else:
+        os.chmod(dir, mode)
+    os.chown(dir, uid, gid)
+    os.chmod(dir, mode)   # the above is masked by umask...
+
+def get_data_dir(fsid, t, n):
+    # type: (str, str, Union[int, str]) -> str
+    return os.path.join(args.data_dir, fsid, '%s.%s' % (t, n))
+
+def get_log_dir(fsid):
+    # type: (str) -> str
+    return os.path.join(args.log_dir, fsid)
+
+def make_data_dir_base(fsid, uid, gid):
+    # type: (str, int, int) -> str
+    data_dir_base = os.path.join(args.data_dir, fsid)
+    makedirs(data_dir_base, uid, gid, DATA_DIR_MODE)
+    makedirs(os.path.join(data_dir_base, 'crash'), uid, gid, DATA_DIR_MODE)
+    makedirs(os.path.join(data_dir_base, 'crash', 'posted'), uid, gid,
+             DATA_DIR_MODE)
+    return data_dir_base
+
+def make_data_dir(fsid, daemon_type, daemon_id, uid=None, gid=None):
+    # type: (str, str, Union[int, str], int, int) -> str
+    if not uid or not gid:
+        (uid, gid) = extract_uid_gid()
+    make_data_dir_base(fsid, uid, gid)
+    data_dir = get_data_dir(fsid, daemon_type, daemon_id)
+    makedirs(data_dir, uid, gid, DATA_DIR_MODE)
+    return data_dir
+
+def make_log_dir(fsid, uid=None, gid=None):
+    # type: (str, int, int) -> str
+    if not uid or not gid:
+        (uid, gid) = extract_uid_gid()
+    log_dir = get_log_dir(fsid)
+    makedirs(log_dir, uid, gid, LOG_DIR_MODE)
+    return log_dir
+
+def copy_files(src, dst, uid=None, gid=None):
+    # type: (List[str], str, int, int) -> None
+    """
+    Copy a files from src to dst
+    """
+    if not uid or not gid:
+        (uid, gid) = extract_uid_gid()
+
+    for src_file in src:
+        dst_file = dst
+        if os.path.isdir(dst):
+            dst_file = os.path.join(dst, os.path.basename(src_file))
+
+        logger.debug('copy file \'%s\' -> \'%s\'' % (src_file, dst_file))
+        shutil.copyfile(src_file, dst_file)
+
+        logger.debug('chown %s:%s \'%s\'' % (uid, gid, dst_file))
+        os.chown(dst_file, uid, gid)
+
+def move_files(src, dst, uid=None, gid=None):
+    # type: (List[str], str, int, int) -> None
+    """
+    Move files from src to dst
+    """
+    if not uid or not gid:
+        (uid, gid) = extract_uid_gid()
+
+    for src_file in src:
+        dst_file = dst
+        if os.path.isdir(dst):
+            dst_file = os.path.join(dst, os.path.basename(src_file))
+
+        if os.path.islink(src_file):
+            # shutil.move() in py2 does not handle symlinks correctly
+            src_rl = os.readlink(src_file)
+            logger.debug("symlink '%s' -> '%s'" % (dst_file, src_rl))
+            os.symlink(src_rl, dst_file)
+            os.unlink(src_file)
+        else:
+            logger.debug("move file '%s' -> '%s'" % (src_file, dst_file))
+            shutil.move(src_file, dst_file)
+            logger.debug('chown %s:%s \'%s\'' % (uid, gid, dst_file))
+            os.chown(dst_file, uid, gid)
+
+def find_program(filename):
+    # type: (str) -> str
+    name = find_executable(filename)
+    if name is None:
+        raise ValueError('%s not found' % filename)
+    return name
+
+def get_unit_name(fsid, daemon_type, daemon_id=None):
+    # type (str, str, Optional[Union[int, str]]) -> str
+    # accept either name or type + id
+    if daemon_id is not None:
+        return 'ceph-%s@%s.%s' % (fsid, daemon_type, daemon_id)
+    else:
+        return 'ceph-%s@%s' % (fsid, daemon_type)
+
+def check_unit(unit_name):
+    # type: (str) -> Tuple[bool, str]
+    # NOTE: we ignore the exit code here because systemctl outputs
+    # various exit codes based on the state of the service, but the
+    # string result is more explicit (and sufficient).
+    enabled = False
+    try:
+        out, err, code = call(['systemctl', 'is-enabled', unit_name],
+                              verbose_on_failure=False)
+        if code == 0:
+            enabled = True
+    except Exception as e:
+        logger.warning('unable to run systemctl: %s' % e)
+        enabled = False
+
+    state = 'unknown'
+    try:
+        out, err, code = call(['systemctl', 'is-active', unit_name],
+                              verbose_on_failure=False)
+        out = out.strip()
+        if out in ['active']:
+            state = 'running'
+        elif out in ['inactive']:
+            state = 'stopped'
+        elif out in ['failed', 'auto-restart']:
+            state = 'error'
+        else:
+            state = 'unknown'
+    except Exception as e:
+        logger.warning('unable to run systemctl: %s' % e)
+        state = 'unknown'
+    return (enabled, state)
+
+def get_legacy_config_fsid(cluster, legacy_dir=None):
+    # type: (str, str) -> Optional[str]
+    config_file = '/etc/ceph/%s.conf' % cluster
+    if legacy_dir is not None:
+        config_file = os.path.abspath(legacy_dir + config_file)
+
+    config = read_config(config_file)
+
+    if config.has_section('global') and config.has_option('global', 'fsid'):
+        return config.get('global', 'fsid')
+    return None
+
+def get_legacy_daemon_fsid(cluster, daemon_type, daemon_id, legacy_dir=None):
+    # type: (str, str, Union[int, str], str) -> Optional[str]
+    fsid = None
+    if daemon_type == 'osd':
+        try:
+            fsid_file = os.path.join(args.data_dir,
+                                     daemon_type,
+                                     'ceph-%s' % daemon_id,
+                                     'ceph_fsid')
+            if legacy_dir is not None:
+                fsid_file = os.path.abspath(legacy_dir + fsid_file)
+            with open(fsid_file, 'r') as f:
+                fsid = f.read().strip()
+        except IOError:
+            pass
+    if not fsid:
+        fsid = get_legacy_config_fsid(cluster, legacy_dir=legacy_dir)
+    return fsid
+
+def get_daemon_args(fsid, daemon_type, daemon_id):
+    # type: (str, str, Union[int, str]) -> List[str]
+    r = [
+        '--default-log-to-file=false',
+        '--default-log-to-stderr=true',
+        ]
+    r += ['--setuser', 'ceph']
+    r += ['--setgroup', 'ceph']
+    return r
+
+def create_daemon_dirs(fsid, daemon_type, daemon_id, uid, gid,
+                       config=None, keyring=None):
+    # type: (str, str, Union[int, str], int, int, str, str) ->  None
+    data_dir = make_data_dir(fsid, daemon_type, daemon_id, uid=uid, gid=gid)
+    make_log_dir(fsid)
+
+    if config:
+        with open(data_dir + '/config', 'w') as f:
+            os.fchown(f.fileno(), uid, gid)
+            os.fchmod(f.fileno(), 0o600)
+            f.write(config)
+    if keyring:
+        with open(data_dir + '/keyring', 'w') as f:
+            os.fchmod(f.fileno(), 0o600)
+            os.fchown(f.fileno(), uid, gid)
+            f.write(keyring)
+
+def get_config_and_keyring():
+    # type: () -> Tuple[str, str]
+    if args.config_and_keyring:
+        if args.config_and_keyring == '-':
+            try:
+                j = injected_stdin # type: ignore
+            except NameError:
+                j = sys.stdin.read()
+        else:
+            with open(args.config_and_keyring, 'r') as f:
+                j = f.read()
+        d = json.loads(j)
+        config = d.get('config')
+        keyring = d.get('keyring')
+    else:
+        if args.key:
+            keyring = '[%s]\n\tkey = %s\n' % (args.name, args.key)
+        elif args.keyring:
+            with open(args.keyring, 'r') as f:
+                keyring = f.read()
+        else:
+            raise Error('no keyring provided')
+        with open(args.config, 'r') as f:
+            config = f.read()
+    return (config, keyring)
+
+def get_config_and_both_keyrings():
+    # type: () -> Tuple[str, str, Optional[str]]
+    if args.config_and_keyrings:
+        if args.config_and_keyrings == '-':
+            try:
+                j = injected_stdin # type: ignore
+            except NameError:
+                j = sys.stdin.read()
+        else:
+            with open(args.config_and_keyrings, 'r') as f:
+                j = f.read()
+        d = json.loads(j)
+        return (d.get('config'), d.get('keyring'), d.get('crash_keyring'))
+    else:
+        if args.key:
+            keyring = '[%s]\n\tkey = %s\n' % (args.name, args.key)
+        elif args.keyring:
+            with open(args.keyring, 'r') as f:
+                keyring = f.read()
+        else:
+            raise Error('no keyring provided')
+        crash_keyring = None
+        if args.crash_keyring:
+            with open(args.crash_keyring, 'r') as f:
+                crash_keyring = f.read()
+        with open(args.config, 'r') as f:
+            config = f.read()
+        return (config, keyring, crash_keyring)
+
+def get_container_mounts(fsid, daemon_type, daemon_id):
+    # type: (str, str, Union[int, str, None]) -> Dict[str, str]
+    mounts = {}
+    if fsid:
+        run_path = os.path.join('/var/run/ceph', fsid);
+        if os.path.exists(run_path):
+            mounts[run_path] = '/var/run/ceph:z'
+        log_dir = get_log_dir(fsid)
+        mounts[log_dir] = '/var/log/ceph:z'
+        crash_dir = '/var/lib/ceph/%s/crash' % fsid
+        if os.path.exists(crash_dir):
+            mounts[crash_dir] = '/var/lib/ceph/crash:z'
+
+    if daemon_id:
+        data_dir = get_data_dir(fsid, daemon_type, daemon_id)
+        if daemon_type == 'rgw':
+            cdata_dir = '/var/lib/ceph/radosgw/ceph-rgw.%s' % (daemon_id)
+        else:
+            cdata_dir = '/var/lib/ceph/%s/ceph-%s' % (daemon_type, daemon_id)
+        mounts[data_dir] = cdata_dir + ':z'
+        mounts[data_dir + '/config'] = '/etc/ceph/ceph.conf:z'
+        if daemon_type == 'rbd-mirror':
+            # rbd-mirror does not search for its keyring in a data directory
+            mounts[data_dir + '/keyring'] = '/etc/ceph/ceph.client.rbd-mirror.%s.keyring' % daemon_id
+
+    if daemon_type in ['mon', 'osd']:
+        mounts['/dev'] = '/dev'  # FIXME: narrow this down?
+        mounts['/run/udev'] = '/run/udev'
+    if daemon_type == 'osd':
+        mounts['/sys'] = '/sys'  # for numa.cc, pick_address, cgroups, ...
+        mounts['/run/lvm'] = '/run/lvm'
+        mounts['/run/lock/lvm'] = '/run/lock/lvm'
+
+    return mounts
+
+def get_container(fsid, daemon_type, daemon_id, privileged=False,
+                  container_args=[]):
+    # type: (str, str, Union[int, str], bool, List[str]) -> CephContainer
+    if daemon_type in ['mon', 'osd'] or privileged:
+        # mon and osd need privileged in order for libudev to query devices
+        container_args += ['--privileged']
+    if daemon_type == 'rgw':
+        entrypoint = '/usr/bin/radosgw'
+        name = 'client.rgw.%s' % daemon_id
+    elif daemon_type == 'rbd-mirror':
+        entrypoint = '/usr/bin/rbd-mirror'
+        name = 'client.rbd-mirror.%s' % daemon_id
+    else:
+        entrypoint = '/usr/bin/ceph-' + daemon_type
+        name = '%s.%s' % (daemon_type, daemon_id)
+    return CephContainer(
+        image=args.image,
+        entrypoint=entrypoint,
+        args=[
+            '-n', name,
+            '-f', # foreground
+        ] + get_daemon_args(fsid, daemon_type, daemon_id),
+        container_args=container_args,
+        volume_mounts=get_container_mounts(fsid, daemon_type, daemon_id),
+        cname='ceph-%s-%s.%s' % (fsid, daemon_type, daemon_id),
+    )
+
+def extract_uid_gid():
+    # type: () -> Tuple[int, int]
+    out = CephContainer(
+        image=args.image,
+        entrypoint='/usr/bin/grep',
+        args=['^ceph:', '/etc/passwd'],
+    ).run()
+    (uid, gid) = out.split(':')[2:4]
+    return (int(uid), int(gid))
+
+def deploy_daemon(fsid, daemon_type, daemon_id, c, uid, gid,
+                  config, keyring):
+    # type: (str, str, Union[int, str], CephContainer, int, int, Optional[str], Optional[str]) -> None
+    if daemon_type == 'mon' and not os.path.exists(
+            get_data_dir(fsid, 'mon', daemon_id)):
+        assert config
+        assert keyring
+        # tmp keyring file
+        tmp_keyring = write_tmp(keyring, uid, gid)
+
+        # tmp config file
+        tmp_config = write_tmp(config, uid, gid)
+
+        # --mkfs
+        create_daemon_dirs(fsid, daemon_type, daemon_id, uid, gid)
+        mon_dir = get_data_dir(fsid, 'mon', daemon_id)
+        log_dir = get_log_dir(fsid)
+        out = CephContainer(
+            image=args.image,
+            entrypoint='/usr/bin/ceph-mon',
+            args=['--mkfs',
+                  '-i', str(daemon_id),
+                  '--fsid', fsid,
+                  '-c', '/tmp/config',
+                  '--keyring', '/tmp/keyring',
+            ] + get_daemon_args(fsid, 'mon', daemon_id),
+            volume_mounts={
+                log_dir: '/var/log/ceph:z',
+                mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % (daemon_id),
+                tmp_keyring.name: '/tmp/keyring:z',
+                tmp_config.name: '/tmp/config:z',
+            },
+        ).run()
+
+        # write conf
+        with open(mon_dir + '/config', 'w') as f:
+            os.fchown(f.fileno(), uid, gid)
+            os.fchmod(f.fileno(), 0o600)
+            f.write(config)
+    else:
+        # dirs, conf, keyring
+        create_daemon_dirs(
+            fsid, daemon_type, daemon_id,
+            uid, gid,
+            config, keyring)
+
+    if daemon_type == 'osd' and args.osd_fsid:
+        pc = CephContainer(
+            image=args.image,
+            entrypoint='/usr/sbin/ceph-volume',
+            args=[
+                'lvm', 'activate',
+                str(daemon_id), args.osd_fsid,
+                '--no-systemd'
+            ],
+            container_args=['--privileged'],
+            volume_mounts=get_container_mounts(fsid, daemon_type, daemon_id),
+            cname='ceph-%s-activate-%s.%s' % (fsid, daemon_type, daemon_id),
+        )
+        pc.run()
+
+    deploy_daemon_units(fsid, uid, gid, daemon_type, daemon_id, c)
+    update_firewalld(daemon_type)
+
+def deploy_daemon_units(fsid, uid, gid, daemon_type, daemon_id, c,
+                        enable=True, start=True):
+    # type: (str, int, int, str, Union[int, str], CephContainer, bool, bool) -> None
+    # cmd
+    data_dir = get_data_dir(fsid, daemon_type, daemon_id)
+    with open(data_dir + '/cmd', 'w') as f:
+        f.write('#!/bin/sh\n' + ' '.join(c.run_cmd()) + '\n')
+        os.fchmod(f.fileno(), 0o700)
+
+    # systemd
+    install_base_units(fsid)
+    unit = get_unit_file(fsid, uid, gid)
+    unit_file = 'ceph-%s@.service' % (fsid)
+    with open(args.unit_dir + '/' + unit_file + '.new', 'w') as f:
+        f.write(unit)
+        os.rename(args.unit_dir + '/' + unit_file + '.new',
+                  args.unit_dir + '/' + unit_file)
+    call_throws(['systemctl', 'daemon-reload'])
+
+    unit_name = get_unit_name(fsid, daemon_type, daemon_id)
+    call(['systemctl', 'stop', unit_name],
+         verbose_on_failure=False)
+    call(['systemctl', 'reset-failed', unit_name],
+         verbose_on_failure=False)
+    if enable:
+        call_throws(['systemctl', 'enable', unit_name])
+    if start:
+        call_throws(['systemctl', 'start', unit_name])
+
+def update_firewalld(daemon_type):
+    if args.skip_firewalld:
+        return
+    cmd = find_executable('firewall-cmd')
+    if not cmd:
+        logger.debug('firewalld does not appear to be present')
+        return
+    (enabled, state) = check_unit('firewalld.service')
+    if not enabled:
+        logger.debug('firewalld.service is not enabled')
+        return
+
+    fw_services = []
+    fw_ports = []
+    if daemon_type == 'mon':
+        fw_services.append('ceph-mon')
+    elif daemon_type in ['mgr', 'mds', 'osd']:
+        fw_services.append('ceph')
+    if daemon_type == 'mgr':
+        fw_ports.append(8080)  # dashboard
+        fw_ports.append(8443)  # dashboard
+        fw_ports.append(9283)  # prometheus
+
+    for svc in fw_services:
+        out, err, ret = call([cmd, '--permanent', '--query-service', svc])
+        if ret:
+            logger.info('Enabling firewalld service %s in current zone...' % svc)
+            out, err, ret = call([cmd, '--permanent', '--add-service', svc])
+            if ret:
+                raise RuntimeError(
+                    'unable to add service %s to current zone: %s' % (svc, err))
+        else:
+            logger.debug('firewalld service %s is enabled in current zone' % svc)
+    for port in fw_ports:
+        port = str(port) + '/tcp'
+        out, err, ret = call([cmd, '--permanent', '--query-port', port])
+        if ret:
+            logger.info('Enabling firewalld port %s in current zone...' % port)
+            out, err, ret = call([cmd, '--permanent', '--add-port', port])
+            if ret:
+                raise RuntimeError('unable to add port %s to current zone: %s' %
+                                   (port, err))
+        else:
+            logger.debug('firewalld port %s is enabled in current zone' % port)
+    call_throws([cmd, '--reload'])
+
+def install_base_units(fsid):
+    # type: (str) -> None
+    """
+    Set up ceph.target and ceph-$fsid.target units.
+    """
+    # global unit
+    existed = os.path.exists(args.unit_dir + '/ceph.target')
+    with open(args.unit_dir + '/ceph.target.new', 'w') as f:
+        f.write('[Unit]\n'
+                'Description=All Ceph clusters and services\n'
+                '\n'
+                '[Install]\n'
+                'WantedBy=multi-user.target\n')
+        os.rename(args.unit_dir + '/ceph.target.new',
+                  args.unit_dir + '/ceph.target')
+    if not existed:
+        # we disable before enable in case a different ceph.target
+        # (from the traditional package) is present; while newer
+        # systemd is smart enough to disable the old
+        # (/lib/systemd/...) and enable the new (/etc/systemd/...),
+        # some older versions of systemd error out with EEXIST.
+        call_throws(['systemctl', 'disable', 'ceph.target'])
+        call_throws(['systemctl', 'enable', 'ceph.target'])
+        call_throws(['systemctl', 'start', 'ceph.target'])
+
+    # cluster unit
+    existed = os.path.exists(args.unit_dir + '/ceph-%s.target' % fsid)
+    with open(args.unit_dir + '/ceph-%s.target.new' % fsid, 'w') as f:
+        f.write('[Unit]\n'
+                'Description=Ceph cluster {fsid}\n'
+                'PartOf=ceph.target\n'
+                'Before=ceph.target\n'
+                '\n'
+                '[Install]\n'
+                'WantedBy=multi-user.target ceph.target\n'.format(
+                    fsid=fsid)
+        )
+        os.rename(args.unit_dir + '/ceph-%s.target.new' % fsid,
+                  args.unit_dir + '/ceph-%s.target' % fsid)
+    if not existed:
+        call_throws(['systemctl', 'enable', 'ceph-%s.target' % fsid])
+        call_throws(['systemctl', 'start', 'ceph-%s.target' % fsid])
+
+    # logrotate for the cluster
+    with open(args.logrotate_dir + '/ceph-%s' % fsid, 'w') as f:
+        """
+        This is a bit sloppy in that the killall/pkill will touch all ceph daemons
+        in all containers, but I don't see an elegant way to send SIGHUP *just* to
+        the daemons for this cluster.  (1) systemd kill -s will get the signal to
+        podman, but podman will exit.  (2) podman kill will get the signal to the
+        first child (bash), but that isn't the ceph daemon.  This is simpler and
+        should be harmless.
+        """
+        f.write("""# created by ceph-daemon
+/var/log/ceph/%s/*.log {
+    rotate 7
+    daily
+    compress
+    sharedscripts
+    postrotate
+        killall -q -1 ceph-mon ceph-mgr ceph-mds ceph-osd ceph-fuse radosgw || pkill -1 -x "ceph-mon|ceph-mgr|ceph-mds|ceph-osd|ceph-fuse|radosgw" || true
+    endscript
+    missingok
+    notifempty
+    su root root
+}
+""" % fsid)
+
+def deploy_crash(fsid, uid, gid, config, keyring):
+    # type: (str, int, int, str, str) -> None
+    crash_dir = os.path.join(args.data_dir, fsid, 'crash')
+    makedirs(crash_dir, uid, gid, DATA_DIR_MODE)
+
+    with open(os.path.join(crash_dir, 'keyring'), 'w') as f:
+        os.fchmod(f.fileno(), 0o600)
+        os.fchown(f.fileno(), uid, gid)
+        f.write(keyring)
+    with open(os.path.join(crash_dir, 'config'), 'w') as f:
+        os.fchmod(f.fileno(), 0o600)
+        os.fchown(f.fileno(), uid, gid)
+        f.write(config)
+
+    # ceph-crash unit
+    mounts = {
+        crash_dir: '/var/lib/ceph/crash:z',
+        os.path.join(crash_dir, 'config'): '/etc/ceph/ceph.conf:z',
+        os.path.join(crash_dir, 'keyring'): '/etc/ceph/ceph.keyring:z',
+    }
+    c = CephContainer(
+        image=args.image,
+        entrypoint='/usr/bin/ceph-crash',
+        args=['-n', 'client.crash.%s' % get_hostname()],
+        volume_mounts=mounts,
+        cname='ceph-%s-crash' % (fsid),
+    )
+    unit_name = 'ceph-%s-crash.service' % fsid
+    with open(os.path.join(args.unit_dir, unit_name + '.new'), 'w') as f:
+        f.write('[Unit]\n'
+                'Description=Ceph cluster {fsid} crash dump collector\n'
+                'PartOf=ceph-{fsid}.target\n'
+                'Before=ceph-{fsid}.target\n'
+                '\n'
+                '[Service]\n'
+                'Type=simple\n'
+                'ExecStart={cmd}\n'
+                'ExecStop=-{container_path} rm -f ceph-{fsid}-crash\n'
+                'Restart=always\n'
+                'RestartSec=10\n'
+                'StartLimitInterval=10min\n'
+                'StartLimitBurst=10\n'
+                '\n'
+                '[Install]\n'
+                'WantedBy=ceph-{fsid}.target\n'.format(
+                    container_path=container_path,
+                    fsid=fsid,
+                    cmd=' '.join(c.run_cmd()))
+        )
+        os.rename(os.path.join(args.unit_dir, unit_name + '.new'),
+                  os.path.join(args.unit_dir, unit_name))
+    subprocess.check_output(['systemctl', 'enable', unit_name])
+    subprocess.check_output(['systemctl', 'start', unit_name])
+
+def get_unit_file(fsid, uid, gid):
+    # type: (str, int, int) -> str
+    install_path = find_program('install')
+    u = """[Unit]
+Description=Ceph daemon for {fsid}
+
+# According to:
+#   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
+Wants=network-online.target local-fs.target time-sync.target
+
+PartOf=ceph-{fsid}.target
+Before=ceph-{fsid}.target
+
+[Service]
+LimitNOFILE=1048576
+LimitNPROC=1048576
+EnvironmentFile=-/etc/environment
+ExecStartPre=-{container_path} rm ceph-{fsid}-%i
+ExecStartPre=-{install_path} -d -m0770 -o {uid} -g {gid} /var/run/ceph/{fsid}
+ExecStart=/bin/bash {data_dir}/{fsid}/%i/cmd
+ExecStop=-{container_path} rm -f ceph-{fsid}-%i
+Restart=on-failure
+RestartSec=10s
+TimeoutStartSec=120
+TimeoutStopSec=15
+StartLimitInterval=30min
+StartLimitBurst=5
+
+[Install]
+WantedBy=ceph-{fsid}.target
+""".format(
+    container_path=container_path,
+    install_path=install_path,
+    fsid=fsid,
+    uid=uid,
+    gid=gid,
+    data_dir=args.data_dir)
+    return u
+
+##################################
+
+class CephContainer:
+    def __init__(self,
+                 image,
+                 entrypoint,
+                 args=[],
+                 volume_mounts={},
+                 cname='',
+                 container_args=[]):
+        # type: (str, str, List[str], Dict[str, str], str, List[str]) -> None
+        self.image = image
+        self.entrypoint = entrypoint
+        self.args = args
+        self.volume_mounts = volume_mounts
+        self.cname = cname
+        self.container_args = container_args
+
+    def run_cmd(self):
+        # type: () -> List[str]
+        vols = [] # type: List[str]
+        envs = [] # type: List[str]
+        cname = [] # type: List[str]
+        vols = sum(
+            [['-v', '%s:%s' % (host_dir, container_dir)]
+             for host_dir, container_dir in self.volume_mounts.items()], [])
+        envs = [
+            '-e', 'CONTAINER_IMAGE=%s' % self.image,
+            '-e', 'NODE_NAME=%s' % get_hostname(),
+        ]
+        cname = ['--name', self.cname] if self.cname else []
+        return [
+            str(container_path),
+            'run',
+            '--rm',
+            '--net=host',
+        ] + self.container_args + \
+        cname + envs + \
+        vols + \
+        [
+            '--entrypoint', self.entrypoint,
+            self.image
+        ] + self.args # type: ignore
+
+    def shell_cmd(self, cmd):
+        # type: (List[str]) -> List[str]
+        vols = [] # type: List[str]
+        vols = sum(
+            [['-v', '%s:%s' % (host_dir, container_dir)]
+             for host_dir, container_dir in self.volume_mounts.items()], [])
+        envs = [
+            '-e', 'CONTAINER_IMAGE=%s' % self.image,
+            '-e', 'NODE_NAME=%s' % get_hostname(),
+        ]
+        cmd_args = [] # type: List[str]
+        if cmd:
+            cmd_args = ['-c'] + cmd
+        return [
+            str(container_path),
+            'run',
+            '--net=host',
+        ] + self.container_args + envs + vols + [
+            '--entrypoint', cmd[0],
+            self.image
+        ] + cmd[1:]
+
+    def exec_cmd(self, cmd):
+        # type: (List[str]) -> List[str]
+        return [
+            str(container_path),
+            'exec',
+        ] + self.container_args + [
+            self.cname,
+        ] + cmd
+
+    def run(self):
+        # type: () -> str
+        logger.debug(self.run_cmd())
+        out, _, _ = call_throws(self.run_cmd(), desc=self.entrypoint)
+        return out
+
+
+##################################
+
+def command_version():
+    # type: () -> int
+    out = CephContainer(args.image, 'ceph', ['--version']).run()
+    print(out.strip())
+    return 0
+
+##################################
+
+def command_pull():
+    # type: () -> None
+    logger.info('Pulling latest %s...' % args.image)
+    call_throws([container_path, 'pull', args.image])
+    out, err, ret = call_throws([
+        container_path, 'inspect',
+        '--format', '{{.Id}}',
+        args.image])
+    print(out.strip())
+
+##################################
+
+def command_bootstrap():
+    # type: () -> int
+
+    # verify output files
+    if not args.allow_overwrite:
+        for f in [args.output_config, args.output_keyring, args.output_pub_ssh_key]:
+            if os.path.exists(f):
+                raise Error('%s already exists; delete or pass '
+                              '--allow-overwrite to overwrite' % f)
+
+    # initial vars
+    fsid = args.fsid or make_fsid()
+    hostname = get_hostname()
+    mon_id = args.mon_id or hostname
+    mgr_id = args.mgr_id or generate_service_id()
+    logging.info('Cluster fsid: %s' % fsid)
+
+    # config
+    cp = read_config(args.config)
+    if args.mon_ip:
+        addr_arg = '[v2:%s:3300,v1:%s:6789]' % (args.mon_ip, args.mon_ip)
+        mon_ip = args.mon_ip
+    elif args.mon_addrv:
+        addr_arg = args.mon_addrv
+        mon_ip = args.mon_addrv.split(':')[1]
+    else:
+        raise Error('must specify --mon-ip or --mon-addrv')
+    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', 'container_image', args.image)
+    cpf = StringIO()
+    cp.write(cpf)
+    config = cpf.getvalue()
+
+    if not args.skip_ping_check:
+        logger.info('Verifying we can ping mon IP %s...' % mon_ip)
+        _, _, ret = call(['timeout', '5', 'ping', mon_ip, '-c', '1'], 'ping')
+        if ret:
+            raise Error('failed to ping %s' % mon_ip)
+
+    if not args.skip_pull:
+        logger.info('Pulling latest %s container...' % args.image)
+        call_throws([container_path, 'pull', args.image])
+
+    logger.info('Extracting ceph user uid/gid from container image...')
+    (uid, gid) = extract_uid_gid()
+
+    # create some initial keys
+    logger.info('Creating initial keys...')
+    mon_key = CephContainer(
+        image=args.image,
+        entrypoint='/usr/bin/ceph-authtool',
+        args=['--gen-print-key'],
+    ).run().strip()
+    admin_key = CephContainer(
+        image=args.image,
+        entrypoint='/usr/bin/ceph-authtool',
+        args=['--gen-print-key'],
+    ).run().strip()
+    mgr_key = CephContainer(
+        image=args.image,
+        entrypoint='/usr/bin/ceph-authtool',
+        args=['--gen-print-key'],
+    ).run().strip()
+    crash_key = CephContainer(
+        image=args.image,
+        entrypoint='/usr/bin/ceph-authtool',
+        args=['--gen-print-key'],
+    ).run().strip()
+
+    keyring = ('[mon.]\n'
+               '\tkey = %s\n'
+               '\tcaps mon = allow *\n'
+               '[client.admin]\n'
+               '\tkey = %s\n'
+               '\tcaps mon = allow *\n'
+               '\tcaps mds = allow *\n'
+               '\tcaps mgr = allow *\n'
+               '\tcaps osd = allow *\n'
+               '[mgr.%s]\n'
+               '\tkey = %s\n'
+               '\tcaps mon = profile mgr\n'
+               '\tcaps mds = allow *\n'
+               '\tcaps osd = allow *\n'
+               '[client.crash.%s]\n'
+               '\tkey = %s\n'
+               '\tcaps mon = profile crash\n'
+               '\tcaps mgr = profile crash\n'
+               % (mon_key, admin_key, mgr_id, mgr_key, hostname, crash_key))
+
+    # tmp keyring file
+    tmp_bootstrap_keyring = write_tmp(keyring, uid, gid)
+
+    # create initial monmap, tmp monmap file
+    logger.info('Creating initial monmap...')
+    tmp_monmap = write_tmp('', 0, 0)
+    out = CephContainer(
+        image=args.image,
+        entrypoint='/usr/bin/monmaptool',
+        args=['--create',
+              '--clobber',
+              '--fsid', fsid,
+              '--addv', mon_id, addr_arg,
+              '/tmp/monmap'
+        ],
+        volume_mounts={
+            tmp_monmap.name: '/tmp/monmap:z',
+        },
+    ).run()
+
+    # pass monmap file to ceph user for use by ceph-mon --mkfs below
+    os.fchown(tmp_monmap.fileno(), uid, gid)
+
+    # create mon
+    logger.info('Creating mon...')
+    create_daemon_dirs(fsid, 'mon', mon_id, uid, gid)
+    mon_dir = get_data_dir(fsid, 'mon', mon_id)
+    log_dir = get_log_dir(fsid)
+    out = CephContainer(
+        image=args.image,
+        entrypoint='/usr/bin/ceph-mon',
+        args=['--mkfs',
+              '-i', mon_id,
+              '--fsid', fsid,
+              '-c', '/dev/null',
+              '--monmap', '/tmp/monmap',
+              '--keyring', '/tmp/keyring',
+        ] + get_daemon_args(fsid, 'mon', mon_id),
+        volume_mounts={
+            log_dir: '/var/log/ceph:z',
+            mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % (mon_id),
+            tmp_bootstrap_keyring.name: '/tmp/keyring:z',
+            tmp_monmap.name: '/tmp/monmap:z',
+        },
+    ).run()
+
+    with open(mon_dir + '/config', 'w') as f:
+        os.fchown(f.fileno(), uid, gid)
+        os.fchmod(f.fileno(), 0o600)
+        f.write(config)
+
+    mon_c = get_container(fsid, 'mon', mon_id)
+    deploy_daemon(fsid, 'mon', mon_id, mon_c, uid, gid,
+                  config=None, keyring=None)
+
+    # client.admin key + config to issue various CLI commands
+    tmp_admin_keyring = write_tmp('[client.admin]\n'
+                                  '\tkey = ' + admin_key + '\n',
+                                       uid, gid)
+    tmp_config = write_tmp(config, uid, gid)
+
+    # a CLI helper to reduce our typing
+    def cli(cmd, extra_mounts={}):
+        # type: (List[str], Dict[str, str]) -> str
+        mounts = {
+            log_dir: '/var/log/ceph:z',
+            tmp_admin_keyring.name: '/etc/ceph/ceph.client.admin.keyring:z',
+            tmp_config.name: '/etc/ceph/ceph.conf:z',
+        }
+        for k, v in extra_mounts.items():
+            mounts[k] = v
+        return CephContainer(
+            image=args.image,
+            entrypoint='/usr/bin/ceph',
+            args=cmd,
+            volume_mounts=mounts,
+        ).run()
+
+    logger.info('Waiting for mon to start...')
+    while True:
+        c = CephContainer(
+            image=args.image,
+            entrypoint='/usr/bin/ceph',
+            args=[
+                'status'],
+            volume_mounts={
+                mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % (mon_id),
+                tmp_admin_keyring.name: '/etc/ceph/ceph.client.admin.keyring:z',
+                tmp_config.name: '/etc/ceph/ceph.conf:z',
+            },
+        )
+        out, err, ret = call(c.run_cmd(), c.entrypoint)
+        if ret == 0:
+            break
+        logger.info('mon is still not available yet, waiting...')
+        time.sleep(1)
+
+    # assimilate and minimize config
+    if not args.no_minimize_config:
+        logger.info('Assimilating anything we can from ceph.conf...')
+        cli([
+            'config', 'assimilate-conf',
+            '-i', '/var/lib/ceph/mon/ceph-%s/config' % mon_id
+        ], {
+            mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % mon_id
+        })
+        logger.info('Generating new minimal ceph.conf...')
+        cli([
+            'config', 'generate-minimal-conf',
+            '-o', '/var/lib/ceph/mon/ceph-%s/config' % mon_id
+        ], {
+            mon_dir: '/var/lib/ceph/mon/ceph-%s:z' % mon_id
+        })
+        # re-read our minimized config
+        with open(mon_dir + '/config', 'r') as f:
+            config = f.read()
+        logger.info('Restarting the monitor...')
+        call_throws([
+            'systemctl',
+            'restart',
+            get_unit_name(fsid, 'mon', mon_id)
+        ])
+
+    # create mgr
+    logger.info('Creating mgr...')
+    mgr_keyring = '[mgr.%s]\n\tkey = %s\n' % (mgr_id, mgr_key)
+    mgr_c = get_container(fsid, 'mgr', mgr_id)
+    deploy_daemon(fsid, 'mgr', mgr_id, mgr_c, uid, gid, config, mgr_keyring)
+
+    # crash unit
+    logger.info('Creating crash agent...')
+    deploy_crash(fsid, uid, gid, config,
+                 '[client.crash.%s]\n\tkey = %s\n' % (hostname, crash_key))
+
+    # output files
+    with open(args.output_keyring, 'w') as f:
+        os.fchmod(f.fileno(), 0o600)
+        f.write('[client.admin]\n'
+                '\tkey = ' + admin_key + '\n')
+    logger.info('Wrote keyring to %s' % args.output_keyring)
+
+    with open(args.output_config, 'w') as f:
+        f.write(config)
+    logger.info('Wrote config to %s' % args.output_config)
+
+    logger.info('Waiting for mgr to start...')
+    while True:
+        out = cli(['status', '-f', 'json-pretty'])
+        j = json.loads(out)
+        if j.get('mgrmap', {}).get('available', False):
+            break
+        logger.info('mgr is still not available yet, waiting...')
+        time.sleep(1)
+
+    # ssh
+    if not args.skip_ssh:
+        logger.info('Enabling ssh module...')
+        cli(['mgr', 'module', 'enable', 'ssh'])
+        logger.info('Setting orchestrator backend to ssh...')
+        cli(['orchestrator', 'set', 'backend', 'ssh'])
+
+        logger.info('Generating ssh key...')
+        cli(['ssh', 'generate-key'])
+        ssh_pub = cli(['ssh', 'get-pub-key'])
+
+        with open(args.output_pub_ssh_key, 'w') as f:
+            f.write(ssh_pub)
+        logger.info('Wrote public SSH key to to %s' % args.output_pub_ssh_key)
+
+        logger.info('Adding key to root@localhost\'s authorized_keys...')
+        if not os.path.exists('/root/.ssh'):
+            os.mkdir('/root/.ssh', 0o700)
+        auth_keys_file = '/root/.ssh/authorized_keys'
+        add_newline = False
+        if os.path.exists(auth_keys_file):
+            with open(auth_keys_file, 'r') as f:
+                f.seek(0, os.SEEK_END)
+                if f.tell() > 0:
+                    f.seek(f.tell()-1, os.SEEK_SET) # go to last char
+                    if f.read() != '\n':
+                        add_newline = True
+        with open(auth_keys_file, 'a') as f:
+            os.fchmod(f.fileno(), 0o600)  # just in case we created it
+            if add_newline:
+                f.write('\n')
+            f.write(ssh_pub.strip() + '\n')
+
+        host = get_hostname()
+        logger.info('Adding host %s...' % host)
+        cli(['orchestrator', 'host', 'add', host])
+
+    if not args.skip_dashboard:
+        logger.info('Enabling the dashboard module...')
+        cli(['mgr', 'module', 'enable', 'dashboard'])
+        logger.info('Waiting for the module to be available...')
+        # FIXME: potential for an endless loop?
+        while True:
+            c_out = cli(['-h'])
+            if 'dashboard' in c_out:
+                break
+            logger.info('Dashboard not yet available, waiting...')
+            time.sleep(1)
+        logger.info('Generating a dashboard self-signed certificate...')
+        cli(['dashboard', 'create-self-signed-cert'])
+        logger.info('Creating initial admin user...')
+        password = args.initial_dashboard_password or generate_password()
+        cli(['dashboard', 'ac-user-create',
+             args.initial_dashboard_user, password,
+             'administrator'])
+        logger.info('Fetching dashboard port number...')
+        out = cli(['config', 'get', 'mgr', 'mgr/dashboard/ssl_server_port'])
+        port = int(out)
+        logger.info('Ceph Dashboard is now available at:\n\n'
+                    '\t     URL: https://%s:%s/\n'
+                    '\t    User: %s\n'
+                    '\tPassword: %s\n' % (
+                        get_fqdn(), port,
+                        args.initial_dashboard_user,
+                        password))
+
+    logger.info('You can access the Ceph CLI with:\n\n'
+                '\tsudo %s shell --fsid %s -c %s -k %s\n' % (
+                    sys.argv[0],
+                    fsid,
+                    args.output_config,
+                    args.output_keyring))
+
+    logger.info('Bootstrap complete.')
+    return 0
+
+##################################
+
+def command_deploy():
+    # type: () -> None
+    (daemon_type, daemon_id) = args.name.split('.', 1)
+    if daemon_type not in ['mon', 'mgr', 'mds', 'osd', 'rgw', 'rbd-mirror']:
+        raise Error('daemon type %s not recognized' % daemon_type)
+    (config, keyring, crash_keyring) = get_config_and_both_keyrings()
+    if daemon_type == 'mon':
+        if args.mon_ip:
+            config += '[mon.%s]\n\tpublic_addr = %s\n' % (daemon_id, args.mon_ip)
+        elif args.mon_addrv:
+            config += '[mon.%s]\n\tpublic_addrv = %s\n' % (daemon_id,
+                                                           args.mon_addrv)
+        elif args.mon_network:
+            config += '[mon.%s]\n\tpublic_network = %s\n' % (daemon_id,
+                                                             args.mon_network)
+        else:
+            raise Error('must specify --mon-ip or --mon-network')
+    (uid, gid) = extract_uid_gid()
+    c = get_container(args.fsid, daemon_type, daemon_id)
+    deploy_daemon(args.fsid, daemon_type, daemon_id, c, uid, gid,
+                  config, keyring)
+    if crash_keyring:
+        deploy_crash(args.fsid, uid, gid, config, crash_keyring)
+
+##################################
+
+def command_run():
+    # type: () -> int
+    (daemon_type, daemon_id) = args.name.split('.', 1)
+    c = get_container(args.fsid, daemon_type, daemon_id)
+    command = c.run_cmd()
+    logger.debug("Running command: %s" % ' '.join(command))
+    return subprocess.call(command)
+
+##################################
+
+@infer_fsid
+def command_shell():
+    # type: () -> int
+    if args.fsid:
+        make_log_dir(args.fsid)
+    if args.name:
+        if '.' in args.name:
+            (daemon_type, daemon_id) = args.name.split('.', 1)
+        else:
+            daemon_type = args.name
+            daemon_id = None
+    else:
+        daemon_type = 'osd'  # get the most mounts
+        daemon_id = None
+    mounts = get_container_mounts(args.fsid, daemon_type, daemon_id)
+    if args.config:
+        mounts[pathify(args.config)] = '/etc/ceph/ceph.conf:z'
+    if args.keyring:
+        mounts[pathify(args.keyring)] = '/etc/ceph/ceph.keyring:z'
+    container_args = ['--privileged']
+    if args.command:
+        command = args.command
+    else:
+        command = ['bash']
+        container_args += [
+            '-it',
+            '-e', 'LANG=C',
+            '-e', "PS1=%s" % CUSTOM_PS1,
+        ]
+    c = CephContainer(
+        image=args.image,
+        entrypoint='doesnotmatter',
+        args=[],
+        container_args=container_args,
+        volume_mounts=mounts)
+    command = c.shell_cmd(command)
+    logger.debug("Running command: %s" % ' '.join(command))
+    return subprocess.call(command)
+
+##################################
+
+@infer_fsid
+def command_enter():
+    # type: () -> int
+    if not args.fsid:
+        raise Error('must pass --fsid to specify cluster')
+    (daemon_type, daemon_id) = args.name.split('.', 1)
+    container_args = [] # type: List[str]
+    if args.command:
+        command = args.command
+    else:
+        command = ['bash']
+        container_args += [
+            '-it',
+            '-e', 'LANG=C',
+            '-e', "PS1=%s" % CUSTOM_PS1,
+        ]
+    c = get_container(args.fsid, daemon_type, daemon_id,
+                      container_args=container_args)
+    command = c.exec_cmd(command)
+    logger.debug("Running command: %s" % ' '.join(command))
+    return subprocess.call(command)
+
+##################################
+
+@infer_fsid
+def command_ceph_volume():
+    # type: () -> None
+    if args.fsid:
+        make_log_dir(args.fsid)
+
+    (uid, gid) = (0, 0) # ceph-volume runs as root
+    mounts = get_container_mounts(args.fsid, 'osd', None)
+
+    tmp_config = None
+    tmp_keyring = None
+
+    if args.config_and_keyring:
+        # note: this will always pull from args.config_and_keyring (we
+        # require it) and never args.config or args.keyring.
+        (config, keyring) = get_config_and_keyring()
+
+        # tmp keyring file
+        tmp_keyring = write_tmp(keyring, uid, gid)
+
+        # tmp config file
+        tmp_config = write_tmp(config, uid, gid)
+
+        mounts[tmp_config.name] = '/etc/ceph/ceph.conf:z'
+        mounts[tmp_keyring.name] = '/var/lib/ceph/bootstrap-osd/ceph.keyring:z'
+
+    c = CephContainer(
+        image=args.image,
+        entrypoint='/usr/sbin/ceph-volume',
+        args=args.command,
+        container_args=['--privileged'],
+        volume_mounts=mounts,
+    )
+    out, err, code = call_throws(c.run_cmd(), verbose=True)
+    if not code:
+        print(out)
+
+##################################
+
+@infer_fsid
+def command_unit():
+    # type: () -> None
+    if not args.fsid:
+        raise Error('must pass --fsid to specify cluster')
+    (daemon_type, daemon_id) = args.name.split('.', 1)
+    unit_name = get_unit_name(args.fsid, daemon_type, daemon_id)
+    call_throws([
+        'systemctl',
+        args.command,
+        unit_name])
+
+##################################
+
+@infer_fsid
+def command_logs():
+    # type: () -> None
+    if not args.fsid:
+        raise Error('must pass --fsid to specify cluster')
+    cmd = [str(container_path), 'logs'] # type: List[str]
+    if args.follow:
+        cmd.append('-f')
+    if args.tail:
+        cmd.append('--tail=' + str(args.tail))
+    cmd.append('ceph-%s-%s' % (args.fsid, args.name))
+
+    # call this directly, without our wrapper, so that we get an unmolested
+    # stdout with logger prefixing.
+    logger.debug("Running command: %s" % ' '.join(cmd))
+    subprocess.call(cmd) # type: ignore
+
+##################################
+
+def command_ls():
+    # type: () -> None
+    ls = list_daemons(detail=not args.no_detail,
+                      legacy_dir=args.legacy_dir)
+    print(json.dumps(ls, indent=4))
+
+def list_daemons(detail=True, legacy_dir=None):
+    # type: (bool, Optional[str]) -> List[Dict[str, str]]
+    host_version = None
+    ls = []
+
+    data_dir = args.data_dir
+    if legacy_dir is not None:
+        data_dir = os.path.abspath(legacy_dir + data_dir)
+
+    # /var/lib/ceph
+    if os.path.exists(data_dir):
+        for i in os.listdir(data_dir):
+            if i in ['mon', 'osd', 'mds', 'mgr']:
+                daemon_type = i
+                for j in os.listdir(os.path.join(data_dir, i)):
+                    if '-' not in j:
+                        continue
+                    (cluster, daemon_id) = j.split('-', 1)
+                    fsid = get_legacy_daemon_fsid(
+                            cluster, daemon_type, daemon_id,
+                            legacy_dir=legacy_dir)
+                    i = {
+                        'style': 'legacy',
+                        'name': '%s.%s' % (daemon_type, daemon_id),
+                        'fsid': fsid if fsid is not None else 'unknown',
+                    }
+                    if detail:
+                        (i['enabled'], i['state']) = check_unit(
+                            'ceph-%s@%s' % (daemon_type, daemon_id))
+                        if not host_version:
+                            try:
+                                out, err, code = call(['ceph', '-v'])
+                                if not code and out.startswith('ceph version '):
+                                    host_version = out.split(' ')[2]
+                            except Exception:
+                                pass
+                        i['host_version'] = host_version
+                    ls.append(i)
+            elif is_fsid(i):
+                fsid = i
+                for j in os.listdir(os.path.join(data_dir, i)):
+                    if j == 'crash':
+                        name = 'crash'
+                        unit_name = 'ceph-%s-crash.service' % fsid
+                    elif '.' in j:
+                        name = j
+                        (daemon_type, daemon_id) = j.split('.', 1)
+                        unit_name = get_unit_name(fsid,
+                                                  daemon_type,
+                                                  daemon_id)
+                    else:
+                        continue
+                    i = {
+                        'style': 'ceph-daemon:v1',
+                        'name': name,
+                        'fsid': fsid,
+                    }
+                    if detail:
+                        # get container id
+                        (i['enabled'], i['state']) = check_unit(unit_name)
+                        container_id = None
+                        image_name = None
+                        image_id = None
+                        version = None
+                        out, err, code = call(
+                            [
+                                container_path, 'inspect',
+                                '--format', '{{.Id}},{{.Config.Image}},{{.Image}}',
+                                'ceph-%s-%s' % (fsid, j)
+                            ],
+                            verbose_on_failure=False)
+                        if not code:
+                            (container_id, image_name, image_id) = out.strip().split(',')
+                            out, err, code = call(
+                                [container_path, 'exec', container_id,
+                                 'ceph', '-v'])
+                            if not code and out.startswith('ceph version '):
+                                version = out.split(' ')[2]
+                        i['container_id'] = container_id
+                        i['container_image_name'] = image_name
+                        i['container_image_id'] = image_id
+                        i['version'] = version
+                    ls.append(i)
+
+    # /var/lib/rook
+    # WRITE ME
+    return ls
+
+
+##################################
+
+def command_adopt():
+    # type: () -> None
+    (daemon_type, daemon_id) = args.name.split('.', 1)
+    (uid, gid) = extract_uid_gid()
+    if args.style == 'legacy':
+        fsid = get_legacy_daemon_fsid(args.cluster,
+                                      daemon_type,
+                                      daemon_id,
+                                      legacy_dir=args.legacy_dir)
+        if not fsid:
+            raise Error('could not detect legacy fsid; set fsid in ceph.conf')
+
+        # NOTE: implicit assumption here that the units correspond to the
+        # cluster we are adopting based on the /etc/{defaults,sysconfig}/ceph
+        # CLUSTER field.
+        unit_name = 'ceph-%s@%s' % (daemon_type, daemon_id)
+        (enabled, state) = check_unit(unit_name)
+
+        if state == 'running':
+            logger.info('Stopping old systemd unit %s...' % unit_name)
+            call_throws(['systemctl', 'stop', unit_name])
+        if enabled:
+            logger.info('Disabling old systemd unit %s...' % unit_name)
+            call_throws(['systemctl', 'disable', unit_name])
+
+        # data
+        logger.info('Moving data...')
+        data_dir_src = ('/var/lib/ceph/%s/%s-%s' %
+                        (daemon_type, args.cluster, daemon_id))
+        data_dir_src = os.path.abspath(args.legacy_dir + data_dir_src)
+        data_dir_dst = make_data_dir(fsid, daemon_type, daemon_id,
+                                     uid=uid, gid=gid)
+        move_files(glob(os.path.join(data_dir_src, '*')),
+                   data_dir_dst,
+                   uid=uid, gid=gid)
+        logger.debug('Remove dir \'%s\'' % (data_dir_src))
+        if os.path.ismount(data_dir_src):
+            call_throws(['umount', data_dir_src])
+        os.rmdir(data_dir_src)
+
+        # config
+        config_src = '/etc/ceph/%s.conf' % (args.cluster)
+        config_src = os.path.abspath(args.legacy_dir + config_src)
+        config_dst = os.path.join(data_dir_dst, 'config')
+        copy_files([config_src], config_dst, uid=uid, gid=gid)
+
+        # logs
+        logger.info('Moving logs...')
+        log_dir_src = ('/var/log/ceph/%s-%s.%s.log*' %
+                        (args.cluster, daemon_type, daemon_id))
+        log_dir_src = os.path.abspath(args.legacy_dir + log_dir_src)
+        log_dir_dst = make_log_dir(fsid, uid=uid, gid=gid)
+        move_files(glob(log_dir_src),
+                   log_dir_dst,
+                   uid=uid, gid=gid)
+
+        logger.info('Creating new units...')
+        c = get_container(fsid, daemon_type, daemon_id)
+        deploy_daemon_units(fsid, uid, gid, daemon_type, daemon_id, c,
+                            enable=True,  # unconditionally enable the new unit
+                            start=(state == 'running'))
+        update_firewalld(daemon_type)
+
+    else:
+        raise Error('adoption of style %s not implemented' % args.style)
+
+##################################
+
+def command_rm_daemon():
+    # type: () -> None
+    (daemon_type, daemon_id) = args.name.split('.', 1)
+    if daemon_type in ['mon', 'osd'] and not args.force:
+        raise Error('must pass --force to proceed: '
+                      'this command may destroy precious data!')
+    unit_name = get_unit_name(args.fsid, daemon_type, daemon_id)
+    call(['systemctl', 'stop', unit_name],
+         verbose_on_failure=False)
+    call(['systemctl', 'reset-failed', unit_name],
+         verbose_on_failure=False)
+    call(['systemctl', 'disable', unit_name],
+         verbose_on_failure=False)
+    data_dir = get_data_dir(args.fsid, daemon_type, daemon_id)
+    call_throws(['rm', '-rf', data_dir])
+
+##################################
+
+def command_rm_cluster():
+    # type: () -> None
+    if not args.force:
+        raise Error('must pass --force to proceed: '
+                      'this command may destroy precious data!')
+
+    # stop + disable individual daemon units
+    for d in list_daemons(detail=False):
+        if d['fsid'] != args.fsid:
+            continue
+        if d['style'] != 'ceph-daemon:v1':
+            continue
+        unit_name = get_unit_name(args.fsid, d['name'])
+        call(['systemctl', 'stop', unit_name],
+             verbose_on_failure=False)
+        call(['systemctl', 'reset-failed', unit_name],
+             verbose_on_failure=False)
+        call(['systemctl', 'disable', unit_name],
+             verbose_on_failure=False)
+
+    # cluster units
+    for unit_name in ['ceph-%s.target' % args.fsid,
+                      'ceph-%s-crash.service' % args.fsid]:
+        call(['systemctl', 'stop', unit_name],
+             verbose_on_failure=False)
+        call(['systemctl', 'reset-failed', unit_name],
+             verbose_on_failure=False)
+        call(['systemctl', 'disable', unit_name],
+             verbose_on_failure=False)
+
+    slice_name = 'system-%s.slice' % (('ceph-%s' % args.fsid).replace('-',
+                                                                      '\\x2d'))
+    call(['systemctl', 'stop', slice_name],
+         verbose_on_failure=False)
+
+    # rm units
+    call_throws(['rm', '-f', args.unit_dir +
+                             '/ceph-%s@.service' % args.fsid])
+    call_throws(['rm', '-f', args.unit_dir +
+                             '/ceph-%s-crash.service' % args.fsid])
+    call_throws(['rm', '-f', args.unit_dir +
+                             '/ceph-%s.target' % args.fsid])
+    call_throws(['rm', '-rf',
+                  args.unit_dir + '/ceph-%s.target.wants' % args.fsid])
+    # rm data
+    call_throws(['rm', '-rf', args.data_dir + '/' + args.fsid])
+    # rm logs
+    call_throws(['rm', '-rf', args.log_dir + '/' + args.fsid])
+    call_throws(['rm', '-rf', args.log_dir +
+                             '/*.wants/ceph-%s@*' % args.fsid])
+    # rm logrotate config
+    call_throws(['rm', '-f', args.logrotate_dir + '/ceph-%s' % args.fsid])
+
+
+##################################
+
+def check_time_sync():
+    units = ['chronyd.service', 'systemd-timesyncd.service', 'ntpd.service']
+    for u in units:
+        (enabled, state) = check_unit(u)
+        if enabled and state == 'running':
+            logger.info('Time sync unit %s is enabled and running' % u)
+            return True
+    logger.warning('No time sync service is running; checked for %s' % units)
+    return False
+
+def command_check_host():
+    # caller already checked for docker/podman
+    logger.info('podman|docker (%s) is present' % container_path)
+
+    if not find_program('systemctl'):
+        raise RuntimeError('unable to location systemctl')
+    logger.info('systemctl is present')
+
+    if not find_program('lvcreate'):
+        raise RuntimeError('LVM does not appear to be installed')
+    logger.info('LVM2 is present')
+
+    # check for configured+running chronyd or ntp
+    if not check_time_sync():
+        raise RuntimeError('No time synchronization is active (checked all of %s)' %
+                           units)
+
+    logger.info('Host looks OK')
+
+
+##################################
+
+def _get_parser():
+    # type: () -> argparse.ArgumentParser
+    parser = argparse.ArgumentParser(
+        description='Bootstrap Ceph daemons with systemd and containers.',
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+    parser.add_argument(
+        '--image',
+        default=os.environ.get('CEPH_DAEMON_IMAGE', DEFAULT_IMAGE),
+        help='container image. Can also be set via the "CEPH_DAEMON_IMAGE" '
+        'env var')
+    parser.add_argument(
+        '--docker',
+        action='store_true',
+        help='use docker instead of podman')
+    parser.add_argument(
+        '--data-dir',
+        default=DATA_DIR,
+        help='base directory for daemon data')
+    parser.add_argument(
+        '--log-dir',
+        default=LOG_DIR,
+        help='base directory for daemon logs')
+    parser.add_argument(
+        '--logrotate-dir',
+        default=LOGROTATE_DIR,
+        help='location of logrotate configuration files')
+    parser.add_argument(
+        '--unit-dir',
+        default=UNIT_DIR,
+        help='base directory for systemd units')
+    parser.add_argument(
+        '--verbose', '-v',
+        action='store_true',
+        help='Show debug-level log messages')
+    subparsers = parser.add_subparsers(help='sub-command')
+
+    parser_version = subparsers.add_parser(
+        'version', help='get ceph version from container')
+    parser_version.set_defaults(func=command_version)
+
+    parser_pull = subparsers.add_parser(
+        'pull', help='pull latest image version')
+    parser_pull.set_defaults(func=command_pull)
+
+    parser_ls = subparsers.add_parser(
+        'ls', help='list daemon instances on this host')
+    parser_ls.set_defaults(func=command_ls)
+    parser_ls.add_argument(
+        '--no-detail',
+        action='store_true',
+        help='Do not include daemon status')
+    parser_ls.add_argument(
+        '--legacy-dir',
+        default='/',
+        help='base directory for legacy daemon data')
+
+    parser_adopt = subparsers.add_parser(
+        'adopt', help='adopt daemon deployed with a different tool')
+    parser_adopt.set_defaults(func=command_adopt)
+    parser_adopt.add_argument(
+        '--name', '-n',
+        required=True,
+        help='daemon name (type.id)')
+    parser_adopt.add_argument(
+        '--style',
+        required=True,
+        help='deployment style (legacy, ...)')
+    parser_adopt.add_argument(
+        '--cluster',
+        default='ceph',
+        help='cluster name')
+    parser_adopt.add_argument(
+        '--legacy-dir',
+        default='/',
+        help='base directory for legacy daemon data')
+    parser_adopt.add_argument(
+        '--skip-firewalld',
+        action='store_true',
+        help='Do not configure firewalld')
+
+    parser_rm_daemon = subparsers.add_parser(
+        'rm-daemon', help='remove daemon instance')
+    parser_rm_daemon.set_defaults(func=command_rm_daemon)
+    parser_rm_daemon.add_argument(
+        '--name', '-n',
+        required=True,
+        help='daemon name (type.id)')
+    parser_rm_daemon.add_argument(
+        '--fsid',
+        required=True,
+        help='cluster FSID')
+    parser_rm_daemon.add_argument(
+        '--force',
+        action='store_true',
+        help='proceed, even though this may destroy valuable data')
+
+    parser_rm_cluster = subparsers.add_parser(
+        'rm-cluster', help='remove all daemons for a cluster')
+    parser_rm_cluster.set_defaults(func=command_rm_cluster)
+    parser_rm_cluster.add_argument(
+        '--fsid',
+        required=True,
+        help='cluster FSID')
+    parser_rm_cluster.add_argument(
+        '--force',
+        action='store_true',
+        help='proceed, even though this may destroy valuable data')
+
+    parser_run = subparsers.add_parser(
+        'run', help='run a ceph daemon, in a container, in the foreground')
+    parser_run.set_defaults(func=command_run)
+    parser_run.add_argument(
+        '--name', '-n',
+        required=True,
+        help='daemon name (type.id)')
+    parser_run.add_argument(
+        '--fsid',
+        required=True,
+        help='cluster FSID')
+
+    parser_shell = subparsers.add_parser(
+        'shell', help='run an interactive shell inside a daemon container')
+    parser_shell.set_defaults(func=command_shell)
+    parser_shell.add_argument(
+        '--fsid',
+        help='cluster FSID')
+    parser_shell.add_argument(
+        '--name', '-n',
+        help='daemon name (type.id)')
+    parser_shell.add_argument(
+        '--config', '-c',
+        help='ceph.conf to pass through to the container')
+    parser_shell.add_argument(
+        '--keyring', '-k',
+        help='ceph.keyring to pass through to the container')
+    parser_shell.add_argument(
+        'command', nargs='*',
+        help='command (optional)')
+
+    parser_enter = subparsers.add_parser(
+        'enter', help='run an interactive shell inside a running daemon container')
+    parser_enter.set_defaults(func=command_enter)
+    parser_enter.add_argument(
+        '--fsid',
+        help='cluster FSID')
+    parser_enter.add_argument(
+        '--name', '-n',
+        required=True,
+        help='daemon name (type.id)')
+    parser_enter.add_argument(
+        'command', nargs='*',
+        help='command')
+
+    parser_ceph_volume = subparsers.add_parser(
+        'ceph-volume', help='run ceph-volume inside a container')
+    parser_ceph_volume.set_defaults(func=command_ceph_volume)
+    parser_ceph_volume.add_argument(
+        '--fsid',
+        help='cluster FSID')
+    parser_ceph_volume.add_argument(
+        '--config-and-keyring',
+        help='JSON file with config and (client.bootrap-osd) key')
+    parser_ceph_volume.add_argument(
+        'command', nargs='+',
+        help='command')
+
+    parser_unit = subparsers.add_parser(
+        'unit', help='operate on the daemon\'s systemd unit')
+    parser_unit.set_defaults(func=command_unit)
+    parser_unit.add_argument(
+        'command',
+        help='systemd command (start, stop, restart, enable, disable, ...)')
+    parser_unit.add_argument(
+        '--fsid',
+        help='cluster FSID')
+    parser_unit.add_argument(
+        '--name', '-n',
+        required=True,
+        help='daemon name (type.id)')
+
+    parser_logs = subparsers.add_parser(
+        'logs', help='fetch the log for a daemon\'s container')
+    parser_logs.set_defaults(func=command_logs)
+    parser_logs.add_argument(
+        '--fsid',
+        help='cluster FSID')
+    parser_logs.add_argument(
+        '--name', '-n',
+        required=True,
+        help='daemon name (type.id)')
+    parser_logs.add_argument(
+        '-f', '--follow',
+        action='store_true',
+        help='Follow log output')
+    parser_logs.add_argument(
+        '--tail',
+        help='Output the specified number of lines at the end of the log')
+
+    parser_bootstrap = subparsers.add_parser(
+        'bootstrap', help='bootstrap a cluster (mon + mgr daemons)')
+    parser_bootstrap.set_defaults(func=command_bootstrap)
+    parser_bootstrap.add_argument(
+        '--config', '-c',
+        help='ceph conf file to incorporate')
+    parser_bootstrap.add_argument(
+        '--mon-id',
+        required=False,
+        help='mon id (default: local hostname)')
+    parser_bootstrap.add_argument(
+        '--mon-addrv',
+        help='mon IPs (e.g., [v2:localipaddr:3300,v1:localipaddr:6789])')
+    parser_bootstrap.add_argument(
+        '--mon-ip',
+        help='mon IP')
+    parser_bootstrap.add_argument(
+        '--mgr-id',
+        required=False,
+        help='mgr id (default: local hostname)')
+    parser_bootstrap.add_argument(
+        '--fsid',
+        help='cluster FSID')
+    parser_bootstrap.add_argument(
+        '--output-keyring',
+        default='ceph.client.admin.keyring',
+        help='location to write keyring file with new cluster admin and mon keys')
+    parser_bootstrap.add_argument(
+        '--output-config',
+        default='ceph.conf',
+        help='location to write conf file to connect to new cluster')
+    parser_bootstrap.add_argument(
+        '--output-pub-ssh-key',
+        default='ceph.pub',
+        help='location to write the cluster\'s public SSH key')
+    parser_bootstrap.add_argument(
+        '--skip-ssh',
+        action='store_true',
+        help='skip setup of ssh key on local host')
+    parser_bootstrap.add_argument(
+        '--initial-dashboard-user',
+        default='admin',
+        help='Initial user for the dashboard')
+    parser_bootstrap.add_argument(
+        '--initial-dashboard-password',
+        help='Initial password for the initial dashboard user')
+    parser_bootstrap.add_argument(
+        '--skip-dashboard',
+        action='store_true',
+        help='do not enable the Ceph Dashboard')
+    parser_bootstrap.add_argument(
+        '--no-minimize-config',
+        action='store_true',
+        help='do not assimilate and minimize the config file')
+    parser_bootstrap.add_argument(
+        '--skip-ping-check',
+        action='store_true',
+        help='do not verify that mon IP is pingable')
+    parser_bootstrap.add_argument(
+        '--skip-pull',
+        action='store_true',
+        help='do not pull the latest image before bootstrapping')
+    parser_bootstrap.add_argument(
+        '--skip-firewalld',
+        action='store_true',
+        help='Do not configure firewalld')
+    parser_bootstrap.add_argument(
+        '--allow-overwrite',
+        action='store_true',
+        help='allow overwrite of existing --output-* config/keyring/ssh files')
+
+    parser_deploy = subparsers.add_parser(
+        'deploy', help='deploy a daemon')
+    parser_deploy.set_defaults(func=command_deploy)
+    parser_deploy.add_argument(
+        '--name',
+        required=True,
+        help='daemon name (type.id)')
+    parser_deploy.add_argument(
+        '--fsid',
+        required=True,
+        help='cluster FSID')
+    parser_deploy.add_argument(
+        '--config', '-c',
+        help='config file for new daemon')
+    parser_deploy.add_argument(
+        '--keyring',
+        help='keyring for new daemon')
+    parser_deploy.add_argument(
+        '--crash-keyring',
+        help='crash keyring for crash agent daemon')
+    parser_deploy.add_argument(
+        '--key',
+        help='key for new daemon')
+    parser_deploy.add_argument(
+        '--config-and-keyrings',
+        help='JSON file with config and keyrings for the daemon and crash agent')
+    parser_deploy.add_argument(
+        '--mon-ip',
+        help='mon IP')
+    parser_deploy.add_argument(
+        '--mon-addrv',
+        help='mon IPs (e.g., [v2:localipaddr:3300,v1:localipaddr:6789])')
+    parser_deploy.add_argument(
+        '--mon-network',
+        help='mon network (CIDR)')
+    parser_deploy.add_argument(
+        '--osd-fsid',
+        help='OSD uuid, if creating an OSD container')
+    parser_deploy.add_argument(
+        '--skip-firewalld',
+        action='store_true',
+        help='Do not configure firewalld')
+
+    parser_check_host = subparsers.add_parser(
+        'check-host', help='check host configuration')
+    parser_check_host.set_defaults(func=command_check_host)
+
+    return parser
+
+
+if __name__ == "__main__":
+    # allow argv to be injected
+    try:
+        av = injected_argv # type: ignore
+    except NameError:
+        av = sys.argv[1:]
+    parser = _get_parser()
+    args = parser.parse_args(av)
+
+    if args.verbose:
+        logging.basicConfig(level=logging.DEBUG)
+    else:
+        logging.basicConfig(level=logging.INFO)
+    logger = logging.getLogger('ceph-daemon')
+
+    # root?
+    if os.geteuid() != 0:
+        sys.stderr.write('ERROR: ceph-daemon should be run as root\n')
+        sys.exit(1)
+
+    # podman or docker?
+    if args.docker:
+        container_path = find_program('docker')
+    else:
+        for i in CONTAINER_PREFERENCE:
+            try:
+                container_path = find_program(i)
+                break
+            except Exception as e:
+                logger.debug('Could not locate %s: %s' % (i, e))
+        if not container_path:
+            sys.stderr.write('Unable to locate any of %s\n' % CONTAINER_PREFERENCE)
+            sys.exit(1)
+
+    if 'func' not in args:
+        sys.stderr.write('No command specified; pass -h or --help for usage\n')
+        sys.exit(1)
+    try:
+        r = args.func()
+    except Error as e:
+        if args.verbose:
+            raise
+        sys.stderr.write('ERROR: %s\n' % e)
+        sys.exit(1)
+    if not r:
+        r = 0
+    sys.exit(r)
diff --git a/src/cephadm/mypy.ini b/src/cephadm/mypy.ini
new file mode 100644 (file)
index 0000000..1215375
--- /dev/null
@@ -0,0 +1,2 @@
+[mypy]
+ignore_missing_imports = True
\ No newline at end of file
diff --git a/src/cephadm/tests/__init__.py b/src/cephadm/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/cephadm/tests/test_ceph_daemon.py b/src/cephadm/tests/test_ceph_daemon.py
new file mode 100644 (file)
index 0000000..ada2bef
--- /dev/null
@@ -0,0 +1,26 @@
+import mock
+import os
+import sys
+import unittest
+
+if sys.version_info >= (3, 3):
+    from importlib.machinery import SourceFileLoader
+    cd = SourceFileLoader('cephadm', 'cephadm').load_module()
+else:
+    import imp
+    cd = imp.load_source('cephadm', 'cephadm')
+
+class TestCephDaemon(unittest.TestCase):
+    def test_is_fsid(self):
+        self.assertFalse(cd.is_fsid('no-uuid'))
+
+    def test__get_parser_image(self):
+        p = cd._get_parser()
+        args = p.parse_args(['--image', 'foo', 'version'])
+        assert args.image == 'foo'
+
+    @mock.patch.dict(os.environ,{'CEPH_DAEMON_IMAGE':'bar'})
+    def test__get_parser_image_with_envvar(self):
+        p = cd._get_parser()
+        args = p.parse_args(['version'])
+        assert args.image == 'bar'
diff --git a/src/cephadm/tox.ini b/src/cephadm/tox.ini
new file mode 100644 (file)
index 0000000..66edfe9
--- /dev/null
@@ -0,0 +1,15 @@
+[tox]
+envlist = py27, py3, mypy
+
+[testenv]
+skipsdist=true
+skip_install=true
+deps =
+  pytest
+  mock
+commands=pytest {posargs}
+
+[testenv:mypy]
+basepython = python3
+deps = mypy
+commands = mypy {posargs:cephadm}
index e4e4c7dd21c6f3af301ca91080c7dbf9b8aeb806..a6918ab3a58a2cba08dfee9c310f4fd5a9c21609 100644 (file)
@@ -5112,9 +5112,9 @@ std::vector<Option> get_global_options() {
     .set_description(""),
 
     Option("ceph_daemon_path", Option::TYPE_STR, Option::LEVEL_ADVANCED)
-    .set_default("/usr/sbin/ceph-daemon")
+    .set_default("/usr/sbin/cephadm")
     .add_service("mgr")
-    .set_description("Path to ceph-daemon utility"),
+    .set_description("Path to cephadm utility"),
 
     Option("mgr_module_path", Option::TYPE_STR, Option::LEVEL_ADVANCED)
     .set_default(CEPH_DATADIR "/mgr")
index 88592e11f21876ba6fa4a4e0712598a45704e189..82a1c39fa7ddcf37c049cbbc5694f55bd31811cd 100644 (file)
@@ -281,7 +281,7 @@ class SSHOrchestrator(MgrModule, orchestrator.Orchestrator):
             with open(path, 'r') as f:
                 self._ceph_daemon = f.read()
         except (IOError, TypeError) as e:
-            raise RuntimeError("unable to read ceph-daemon at '%s': %s" % (
+            raise RuntimeError("unable to read cephadm at '%s': %s" % (
                 path, str(e)))
 
         self._worker_pool = multiprocessing.pool.ThreadPool(1)
@@ -418,7 +418,7 @@ class SSHOrchestrator(MgrModule, orchestrator.Orchestrator):
         if self.mode == 'root':
             self.ssh_user = 'root'
         elif self.mode == 'ceph-daemon-package':
-            self.ssh_user = 'cephdaemon'
+            self.ssh_user = 'cephadm'
 
     @staticmethod
     def can_run():
@@ -633,12 +633,12 @@ class SSHOrchestrator(MgrModule, orchestrator.Orchestrator):
             elif self.mode == 'ceph-daemon-package':
                 out, err, code = remoto.process.check(
                     conn,
-                    ['sudo', '/usr/bin/ceph-daemon'] + final_args,
+                    ['sudo', '/usr/bin/cephadm'] + final_args,
                     stdin=stdin)
             self.log.debug('exit code %s out %s err %s' % (code, out, err))
             if code and not error_ok:
                 raise RuntimeError(
-                    'ceph-daemon exited with an error code: %d, stderr:%s' % (
+                    'cephadm exited with an error code: %d, stderr:%s' % (
                         code, '\n'.join(err)))
             return out, err, code
 
index aa4a767f21be0ed40a25fbae999e2d62ace85dc1..28a7ac18c8b6267863595ecfb52aaf5e20d89553 100755 (executable)
@@ -712,7 +712,7 @@ $extra_conf
 [mgr]
         mgr data = $CEPH_DEV_DIR/mgr.\$id
         mgr module path = $MGR_PYTHON_PATH
-        ceph daemon path = $CEPH_ROOT/src/ceph-daemon/ceph-daemon
+        ceph daemon path = $CEPH_ROOT/src/cephadm/cephadm
 $DAEMONOPTS
 $extra_conf
 [osd]
diff --git a/sudoers.d/cephadm b/sudoers.d/cephadm
new file mode 100644 (file)
index 0000000..4063b3e
--- /dev/null
@@ -0,0 +1,7 @@
+# allow cephadm user to sudo cephadm
+cephadm ALL=NOPASSWD: /usr/bin/cephadm --image * ls
+cephadm ALL=NOPASSWD: /usr/bin/cephadm --image * unit *
+cephadm ALL=NOPASSWD: /usr/bin/cephadm --image * shell *
+cephadm ALL=NOPASSWD: /usr/bin/cephadm --image * deploy *
+cephadm ALL=NOPASSWD: /usr/bin/cephadm --image * ceph-volume *
+cephadm ALL=NOPASSWD: /usr/bin/cephadm --image * rm-daemon *
diff --git a/sudoers.d/cephdaemon b/sudoers.d/cephdaemon
deleted file mode 100644 (file)
index cac61b2..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-# allow cephdaemon user to sudo ceph-daemon
-cephdaemon ALL=NOPASSWD: /usr/bin/ceph-daemon --image * ls
-cephdaemon ALL=NOPASSWD: /usr/bin/ceph-daemon --image * unit *
-cephdaemon ALL=NOPASSWD: /usr/bin/ceph-daemon --image * shell *
-cephdaemon ALL=NOPASSWD: /usr/bin/ceph-daemon --image * deploy *
-cephdaemon ALL=NOPASSWD: /usr/bin/ceph-daemon --image * ceph-volume *
-cephdaemon ALL=NOPASSWD: /usr/bin/ceph-daemon --image * rm-daemon *
index cf4113ba595849a8a60cf6e993a983d1c80e513b..5f68be4a411248162588cfacd0bdcbd5bec817cc 100755 (executable)
@@ -12,7 +12,7 @@ OSD_TO_CREATE=6
 OSD_VG_NAME=${SCRIPT_NAME%.*}
 OSD_LV_NAME=${SCRIPT_NAME%.*}
 
-CEPH_DAEMON=../src/ceph-daemon/ceph-daemon
+CEPH_DAEMON=../src/cephadm/cephadm
 
 #A="-d"