From a16316fc4dd364135b11226df42d9df65c0c60a2 Mon Sep 17 00:00:00 2001 From: Jenkins Build Slave User Date: Thu, 1 Oct 2020 17:52:04 +0000 Subject: [PATCH 1/1] 2.1.0 --- .gitignore | 22 + CONTRIBUTING.rst | 48 ++ LICENSE | 19 + MANIFEST.in | 5 + README.rst | 373 ++++++++++ bootstrap | 105 +++ ceph-deploy.spec | 129 ++++ ceph_deploy/__init__.py | 3 + ceph_deploy/admin.py | 61 ++ ceph_deploy/cli.py | 184 +++++ ceph_deploy/cliutil.py | 8 + ceph_deploy/conf/__init__.py | 2 + ceph_deploy/conf/ceph.py | 102 +++ ceph_deploy/conf/cephdeploy.py | 216 ++++++ ceph_deploy/config.py | 111 +++ ceph_deploy/connection.py | 44 ++ ceph_deploy/exc.py | 127 ++++ ceph_deploy/forgetkeys.py | 37 + ceph_deploy/gatherkeys.py | 285 ++++++++ ceph_deploy/hosts/__init__.py | 154 ++++ ceph_deploy/hosts/alt/__init__.py | 30 + ceph_deploy/hosts/alt/install.py | 43 ++ ceph_deploy/hosts/alt/mon/__init__.py | 2 + ceph_deploy/hosts/alt/uninstall.py | 11 + ceph_deploy/hosts/arch/__init__.py | 26 + ceph_deploy/hosts/arch/install.py | 49 ++ ceph_deploy/hosts/arch/mon/__init__.py | 2 + ceph_deploy/hosts/arch/uninstall.py | 50 ++ ceph_deploy/hosts/centos/__init__.py | 34 + ceph_deploy/hosts/centos/install.py | 219 ++++++ ceph_deploy/hosts/centos/mon/__init__.py | 2 + ceph_deploy/hosts/centos/uninstall.py | 10 + ceph_deploy/hosts/clear/__init__.py | 25 + ceph_deploy/hosts/clear/install.py | 14 + ceph_deploy/hosts/clear/mon/__init__.py | 2 + ceph_deploy/hosts/clear/uninstall.py | 42 ++ ceph_deploy/hosts/common.py | 248 +++++++ ceph_deploy/hosts/debian/__init__.py | 35 + ceph_deploy/hosts/debian/install.py | 125 ++++ ceph_deploy/hosts/debian/mon/__init__.py | 2 + ceph_deploy/hosts/debian/uninstall.py | 15 + ceph_deploy/hosts/fedora/__init__.py | 30 + ceph_deploy/hosts/fedora/install.py | 87 +++ ceph_deploy/hosts/fedora/mon/__init__.py | 2 + ceph_deploy/hosts/fedora/uninstall.py | 8 + ceph_deploy/hosts/remotes.py | 417 +++++++++++ ceph_deploy/hosts/rhel/__init__.py | 33 + ceph_deploy/hosts/rhel/install.py | 71 ++ ceph_deploy/hosts/rhel/mon/__init__.py | 2 + ceph_deploy/hosts/rhel/uninstall.py | 11 + ceph_deploy/hosts/suse/__init__.py | 31 + ceph_deploy/hosts/suse/install.py | 98 +++ ceph_deploy/hosts/suse/mon/__init__.py | 2 + ceph_deploy/hosts/suse/uninstall.py | 10 + ceph_deploy/hosts/util.py | 31 + ceph_deploy/install.py | 670 ++++++++++++++++++ ceph_deploy/lib/__init__.py | 27 + ceph_deploy/lib/vendor/__init__.py | 0 ceph_deploy/mds.py | 226 ++++++ ceph_deploy/mgr.py | 226 ++++++ ceph_deploy/misc.py | 22 + ceph_deploy/mon.py | 596 ++++++++++++++++ ceph_deploy/new.py | 276 ++++++++ ceph_deploy/osd.py | 611 ++++++++++++++++ ceph_deploy/pkg.py | 86 +++ ceph_deploy/repo.py | 113 +++ ceph_deploy/rgw.py | 233 ++++++ ceph_deploy/tests/__init__.py | 0 ceph_deploy/tests/conftest.py | 98 +++ ceph_deploy/tests/directory.py | 13 + ceph_deploy/tests/fakes.py | 9 + ceph_deploy/tests/parser/__init__.py | 0 ceph_deploy/tests/parser/test_admin.py | 33 + ceph_deploy/tests/parser/test_config.py | 60 ++ ceph_deploy/tests/parser/test_disk.py | 88 +++ ceph_deploy/tests/parser/test_gatherkeys.py | 33 + ceph_deploy/tests/parser/test_install.py | 158 +++++ ceph_deploy/tests/parser/test_main.py | 100 +++ ceph_deploy/tests/parser/test_mds.py | 35 + ceph_deploy/tests/parser/test_mon.py | 122 ++++ ceph_deploy/tests/parser/test_new.py | 84 +++ ceph_deploy/tests/parser/test_osd.py | 101 +++ ceph_deploy/tests/parser/test_pkg.py | 66 ++ ceph_deploy/tests/parser/test_purge.py | 33 + ceph_deploy/tests/parser/test_purgedata.py | 33 + ceph_deploy/tests/parser/test_repo.py | 71 ++ ceph_deploy/tests/parser/test_rgw.py | 35 + ceph_deploy/tests/parser/test_uninstall.py | 33 + ceph_deploy/tests/test_cli_admin.py | 60 ++ ceph_deploy/tests/test_cli_mon.py | 56 ++ ceph_deploy/tests/test_cli_new.py | 71 ++ ceph_deploy/tests/test_cli_rgw.py | 11 + ceph_deploy/tests/test_conf.py | 86 +++ ceph_deploy/tests/test_gather_keys.py | 141 ++++ ceph_deploy/tests/test_gather_keys_missing.py | 179 +++++ .../tests/test_gather_keys_with_mon.py | 219 ++++++ ceph_deploy/tests/test_install.py | 149 ++++ ceph_deploy/tests/test_keys_equivalent.py | 171 +++++ ceph_deploy/tests/test_mon.py | 95 +++ ceph_deploy/tests/test_remotes.py | 255 +++++++ ceph_deploy/tests/unit/hosts/test_altlinux.py | 10 + ceph_deploy/tests/unit/hosts/test_centos.py | 64 ++ ceph_deploy/tests/unit/hosts/test_common.py | 24 + ceph_deploy/tests/unit/hosts/test_hosts.py | 437 ++++++++++++ ceph_deploy/tests/unit/hosts/test_remotes.py | 37 + ceph_deploy/tests/unit/hosts/test_suse.py | 34 + ceph_deploy/tests/unit/hosts/test_util.py | 29 + ceph_deploy/tests/unit/test_cli.py | 46 ++ ceph_deploy/tests/unit/test_conf.py | 192 +++++ ceph_deploy/tests/unit/test_exc.py | 16 + ceph_deploy/tests/unit/test_mon.py | 224 ++++++ ceph_deploy/tests/unit/test_new.py | 28 + .../tests/unit/util/test_arg_validators.py | 128 ++++ ceph_deploy/tests/unit/util/test_constants.py | 16 + ceph_deploy/tests/unit/util/test_net.py | 53 ++ ceph_deploy/tests/unit/util/test_packages.py | 43 ++ ceph_deploy/tests/unit/util/test_paths.py | 50 ++ .../tests/unit/util/test_pkg_managers.py | 195 +++++ ceph_deploy/tests/unit/util/test_system.py | 57 ++ ceph_deploy/tests/unit/util/test_templates.py | 29 + ceph_deploy/tests/util.py | 33 + ceph_deploy/util/__init__.py | 11 + ceph_deploy/util/arg_validators.py | 83 +++ ceph_deploy/util/constants.py | 36 + ceph_deploy/util/decorators.py | 112 +++ ceph_deploy/util/files.py | 5 + ceph_deploy/util/help_formatters.py | 33 + ceph_deploy/util/log.py | 67 ++ ceph_deploy/util/net.py | 399 +++++++++++ ceph_deploy/util/packages.py | 74 ++ ceph_deploy/util/paths/__init__.py | 3 + ceph_deploy/util/paths/gpg.py | 8 + ceph_deploy/util/paths/mon.py | 84 +++ ceph_deploy/util/paths/osd.py | 13 + ceph_deploy/util/pkg_managers.py | 479 +++++++++++++ ceph_deploy/util/ssh.py | 32 + ceph_deploy/util/system.py | 180 +++++ ceph_deploy/util/templates.py | 94 +++ ceph_deploy/util/versions.py | 47 ++ ceph_deploy/validate.py | 16 + debian/ceph-deploy.install | 1 + debian/changelog | 377 ++++++++++ debian/compat | 1 + debian/control | 26 + debian/copyright | 3 + debian/rules | 12 + debian/source/format | 1 + docs/Makefile | 177 +++++ docs/source/_static/.empty | 0 .../ceph/static/font/ApexSans-Book.eot | Bin 0 -> 199888 bytes .../ceph/static/font/ApexSans-Book.svg | 1 + .../ceph/static/font/ApexSans-Book.ttf | Bin 0 -> 199616 bytes .../ceph/static/font/ApexSans-Book.woff | Bin 0 -> 64736 bytes .../ceph/static/font/ApexSans-Medium.eot | Bin 0 -> 169448 bytes .../ceph/static/font/ApexSans-Medium.svg | 1 + .../ceph/static/font/ApexSans-Medium.ttf | Bin 0 -> 169168 bytes .../ceph/static/font/ApexSans-Medium.woff | Bin 0 -> 61116 bytes docs/source/_themes/ceph/static/nature.css_t | 325 +++++++++ docs/source/_themes/ceph/theme.conf | 4 + docs/source/admin.rst | 26 + docs/source/changelog.rst | 623 ++++++++++++++++ docs/source/conf.py | 268 +++++++ docs/source/conf.rst | 175 +++++ docs/source/contents.rst | 18 + docs/source/gatherkeys.rst | 55 ++ docs/source/index.rst | 315 ++++++++ docs/source/install.rst | 219 ++++++ docs/source/mds.rst | 20 + docs/source/mon.rst | 106 +++ docs/source/new.rst | 75 ++ docs/source/pkg.rst | 58 ++ docs/source/repo.rst | 77 ++ docs/source/rgw.rst | 36 + requirements-dev.txt | 3 + requirements.txt | 1 + scripts/build-debian.sh | 87 +++ scripts/build-rpm.sh | 59 ++ scripts/ceph-deploy | 21 + scripts/jenkins-build | 53 ++ scripts/jenkins-pull-requests-build | 14 + setup.cfg | 2 + setup.py | 74 ++ tox.ini | 28 + vendor.py | 112 +++ 184 files changed, 16613 insertions(+) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100755 bootstrap create mode 100644 ceph-deploy.spec create mode 100644 ceph_deploy/__init__.py create mode 100644 ceph_deploy/admin.py create mode 100644 ceph_deploy/cli.py create mode 100644 ceph_deploy/cliutil.py create mode 100644 ceph_deploy/conf/__init__.py create mode 100644 ceph_deploy/conf/ceph.py create mode 100644 ceph_deploy/conf/cephdeploy.py create mode 100644 ceph_deploy/config.py create mode 100644 ceph_deploy/connection.py create mode 100644 ceph_deploy/exc.py create mode 100644 ceph_deploy/forgetkeys.py create mode 100644 ceph_deploy/gatherkeys.py create mode 100644 ceph_deploy/hosts/__init__.py create mode 100644 ceph_deploy/hosts/alt/__init__.py create mode 100644 ceph_deploy/hosts/alt/install.py create mode 100644 ceph_deploy/hosts/alt/mon/__init__.py create mode 100644 ceph_deploy/hosts/alt/uninstall.py create mode 100644 ceph_deploy/hosts/arch/__init__.py create mode 100644 ceph_deploy/hosts/arch/install.py create mode 100644 ceph_deploy/hosts/arch/mon/__init__.py create mode 100644 ceph_deploy/hosts/arch/uninstall.py create mode 100644 ceph_deploy/hosts/centos/__init__.py create mode 100644 ceph_deploy/hosts/centos/install.py create mode 100644 ceph_deploy/hosts/centos/mon/__init__.py create mode 100644 ceph_deploy/hosts/centos/uninstall.py create mode 100644 ceph_deploy/hosts/clear/__init__.py create mode 100644 ceph_deploy/hosts/clear/install.py create mode 100644 ceph_deploy/hosts/clear/mon/__init__.py create mode 100644 ceph_deploy/hosts/clear/uninstall.py create mode 100644 ceph_deploy/hosts/common.py create mode 100644 ceph_deploy/hosts/debian/__init__.py create mode 100644 ceph_deploy/hosts/debian/install.py create mode 100644 ceph_deploy/hosts/debian/mon/__init__.py create mode 100644 ceph_deploy/hosts/debian/uninstall.py create mode 100644 ceph_deploy/hosts/fedora/__init__.py create mode 100644 ceph_deploy/hosts/fedora/install.py create mode 100644 ceph_deploy/hosts/fedora/mon/__init__.py create mode 100644 ceph_deploy/hosts/fedora/uninstall.py create mode 100644 ceph_deploy/hosts/remotes.py create mode 100644 ceph_deploy/hosts/rhel/__init__.py create mode 100644 ceph_deploy/hosts/rhel/install.py create mode 100644 ceph_deploy/hosts/rhel/mon/__init__.py create mode 100644 ceph_deploy/hosts/rhel/uninstall.py create mode 100644 ceph_deploy/hosts/suse/__init__.py create mode 100644 ceph_deploy/hosts/suse/install.py create mode 100644 ceph_deploy/hosts/suse/mon/__init__.py create mode 100644 ceph_deploy/hosts/suse/uninstall.py create mode 100644 ceph_deploy/hosts/util.py create mode 100644 ceph_deploy/install.py create mode 100644 ceph_deploy/lib/__init__.py create mode 100644 ceph_deploy/lib/vendor/__init__.py create mode 100644 ceph_deploy/mds.py create mode 100644 ceph_deploy/mgr.py create mode 100644 ceph_deploy/misc.py create mode 100644 ceph_deploy/mon.py create mode 100644 ceph_deploy/new.py create mode 100644 ceph_deploy/osd.py create mode 100644 ceph_deploy/pkg.py create mode 100644 ceph_deploy/repo.py create mode 100644 ceph_deploy/rgw.py create mode 100644 ceph_deploy/tests/__init__.py create mode 100644 ceph_deploy/tests/conftest.py create mode 100644 ceph_deploy/tests/directory.py create mode 100644 ceph_deploy/tests/fakes.py create mode 100644 ceph_deploy/tests/parser/__init__.py create mode 100644 ceph_deploy/tests/parser/test_admin.py create mode 100644 ceph_deploy/tests/parser/test_config.py create mode 100644 ceph_deploy/tests/parser/test_disk.py create mode 100644 ceph_deploy/tests/parser/test_gatherkeys.py create mode 100644 ceph_deploy/tests/parser/test_install.py create mode 100644 ceph_deploy/tests/parser/test_main.py create mode 100644 ceph_deploy/tests/parser/test_mds.py create mode 100644 ceph_deploy/tests/parser/test_mon.py create mode 100644 ceph_deploy/tests/parser/test_new.py create mode 100644 ceph_deploy/tests/parser/test_osd.py create mode 100644 ceph_deploy/tests/parser/test_pkg.py create mode 100644 ceph_deploy/tests/parser/test_purge.py create mode 100644 ceph_deploy/tests/parser/test_purgedata.py create mode 100644 ceph_deploy/tests/parser/test_repo.py create mode 100644 ceph_deploy/tests/parser/test_rgw.py create mode 100644 ceph_deploy/tests/parser/test_uninstall.py create mode 100644 ceph_deploy/tests/test_cli_admin.py create mode 100644 ceph_deploy/tests/test_cli_mon.py create mode 100644 ceph_deploy/tests/test_cli_new.py create mode 100644 ceph_deploy/tests/test_cli_rgw.py create mode 100644 ceph_deploy/tests/test_conf.py create mode 100644 ceph_deploy/tests/test_gather_keys.py create mode 100644 ceph_deploy/tests/test_gather_keys_missing.py create mode 100644 ceph_deploy/tests/test_gather_keys_with_mon.py create mode 100644 ceph_deploy/tests/test_install.py create mode 100644 ceph_deploy/tests/test_keys_equivalent.py create mode 100644 ceph_deploy/tests/test_mon.py create mode 100644 ceph_deploy/tests/test_remotes.py create mode 100644 ceph_deploy/tests/unit/hosts/test_altlinux.py create mode 100644 ceph_deploy/tests/unit/hosts/test_centos.py create mode 100644 ceph_deploy/tests/unit/hosts/test_common.py create mode 100644 ceph_deploy/tests/unit/hosts/test_hosts.py create mode 100644 ceph_deploy/tests/unit/hosts/test_remotes.py create mode 100644 ceph_deploy/tests/unit/hosts/test_suse.py create mode 100644 ceph_deploy/tests/unit/hosts/test_util.py create mode 100644 ceph_deploy/tests/unit/test_cli.py create mode 100644 ceph_deploy/tests/unit/test_conf.py create mode 100644 ceph_deploy/tests/unit/test_exc.py create mode 100644 ceph_deploy/tests/unit/test_mon.py create mode 100644 ceph_deploy/tests/unit/test_new.py create mode 100644 ceph_deploy/tests/unit/util/test_arg_validators.py create mode 100644 ceph_deploy/tests/unit/util/test_constants.py create mode 100644 ceph_deploy/tests/unit/util/test_net.py create mode 100644 ceph_deploy/tests/unit/util/test_packages.py create mode 100644 ceph_deploy/tests/unit/util/test_paths.py create mode 100644 ceph_deploy/tests/unit/util/test_pkg_managers.py create mode 100644 ceph_deploy/tests/unit/util/test_system.py create mode 100644 ceph_deploy/tests/unit/util/test_templates.py create mode 100644 ceph_deploy/tests/util.py create mode 100644 ceph_deploy/util/__init__.py create mode 100644 ceph_deploy/util/arg_validators.py create mode 100644 ceph_deploy/util/constants.py create mode 100644 ceph_deploy/util/decorators.py create mode 100644 ceph_deploy/util/files.py create mode 100644 ceph_deploy/util/help_formatters.py create mode 100644 ceph_deploy/util/log.py create mode 100644 ceph_deploy/util/net.py create mode 100644 ceph_deploy/util/packages.py create mode 100644 ceph_deploy/util/paths/__init__.py create mode 100644 ceph_deploy/util/paths/gpg.py create mode 100644 ceph_deploy/util/paths/mon.py create mode 100644 ceph_deploy/util/paths/osd.py create mode 100644 ceph_deploy/util/pkg_managers.py create mode 100644 ceph_deploy/util/ssh.py create mode 100644 ceph_deploy/util/system.py create mode 100644 ceph_deploy/util/templates.py create mode 100644 ceph_deploy/util/versions.py create mode 100644 ceph_deploy/validate.py create mode 100644 debian/ceph-deploy.install create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 docs/Makefile create mode 100644 docs/source/_static/.empty create mode 100644 docs/source/_themes/ceph/static/font/ApexSans-Book.eot create mode 100644 docs/source/_themes/ceph/static/font/ApexSans-Book.svg create mode 100644 docs/source/_themes/ceph/static/font/ApexSans-Book.ttf create mode 100644 docs/source/_themes/ceph/static/font/ApexSans-Book.woff create mode 100644 docs/source/_themes/ceph/static/font/ApexSans-Medium.eot create mode 100644 docs/source/_themes/ceph/static/font/ApexSans-Medium.svg create mode 100644 docs/source/_themes/ceph/static/font/ApexSans-Medium.ttf create mode 100644 docs/source/_themes/ceph/static/font/ApexSans-Medium.woff create mode 100644 docs/source/_themes/ceph/static/nature.css_t create mode 100644 docs/source/_themes/ceph/theme.conf create mode 100644 docs/source/admin.rst create mode 100644 docs/source/changelog.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/conf.rst create mode 100644 docs/source/contents.rst create mode 100644 docs/source/gatherkeys.rst create mode 100644 docs/source/index.rst create mode 100644 docs/source/install.rst create mode 100644 docs/source/mds.rst create mode 100644 docs/source/mon.rst create mode 100644 docs/source/new.rst create mode 100644 docs/source/pkg.rst create mode 100644 docs/source/repo.rst create mode 100644 docs/source/rgw.rst create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100755 scripts/build-debian.sh create mode 100755 scripts/build-rpm.sh create mode 100755 scripts/ceph-deploy create mode 100755 scripts/jenkins-build create mode 100644 scripts/jenkins-pull-requests-build create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tox.ini create mode 100644 vendor.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efd1a25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*~ +.#* +## the next line needs to start with a backslash to avoid looking like +## a comment +\#*# +.*.swp + +*.pyc +*.pyo +*.egg-info +/build +/dist +build + +/virtualenv +/.tox + +/ceph-deploy +/*.conf + +*/lib/vendor/remoto +remoto diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..60cead5 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,48 @@ +Contributing to ceph-deploy +=========================== +Before any contributions, a reference ticket *must* exist. The community issue +tracker is hosted at tracker.ceph.com + +To open a new issue, requests can go to: + +http://tracker.ceph.com/projects/ceph-deploy/issues/new + + +commits +------- +Once a ticket exists, commits should be prefaced by the ticket ID. This makes +it easier for maintainers to keep track of why a given line changed, mapping +directly to work done on a ticket. + +For tickets coming from tracker.ceph.com, we expect the following format:: + + [RM-0000] this is a commit message for tracker.ceph.com + +``RM`` stands for Redmine which is the software running tracker.ceph.com. +Similarly, if a ticket was created in bugzilla.redhat.com, we expect the +following format:: + + [BZ-0000] this is a commit message for bugzilla.redhat.com + + +To automate this process, you can create a branch with the tracker identifier +and id (replace "0000" with the ticket number):: + + git checkout -b RM-0000 + +And then use the follow prepare-commit-msg: +https://gist.github.com/alfredodeza/6d62d99a95c9a7975fbe + +Copy that file to ``$GITREPOSITORY/.git/hooks/prepare-commit-msg`` +and mark it executable. + +Your commit messages should then be automatically prefixed with the branch name +based off of the issue tracker. + +tests and documentation +----------------------- +Wherever it is feasible, tests must exist and documentation must be added or +improved depending on the change. + +The build process not only runs tests but ensures that docs can be built from +the proposed changes as well. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..26624cf --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Inktank Storage, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..370e3d9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include *.rst +include LICENSE +include scripts/ceph-deploy +include vendor.py +include tox.ini diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..0fffae8 --- /dev/null +++ b/README.rst @@ -0,0 +1,373 @@ +======================================================== + ceph-deploy -- Deploy Ceph with minimal infrastructure +======================================================== + +``ceph-deploy`` is a way to deploy Ceph relying on just SSH access to +the servers, ``sudo``, and some Python. It runs fully on your +workstation, requiring no servers, databases, or anything like that. + +If you set up and tear down Ceph clusters a lot, and want minimal +extra bureaucracy, this is for you. + +This ``README`` provides a brief overview of ceph-deploy, for thorough +documentation please go to https://docs.ceph.com/projects/ceph-deploy/en/latest/ + +.. _what this tool is not: + +What this tool is not +--------------------- +It is not a generic deployment system, it is only for Ceph, and is designed +for users who want to quickly get Ceph running with sensible initial settings +without the overhead of installing Chef, Puppet or Juju. + +It does not handle client configuration beyond pushing the Ceph config file +and users who want fine-control over security settings, partitions or directory +locations should use a tool such as Chef or Puppet. + + +Installation +============ +Depending on what type of usage you are going to have with ``ceph-deploy`` you +might want to look into the different ways to install it. For automation, you +might want to ``bootstrap`` directly. Regular users of ``ceph-deploy`` would +probably install from the OS packages or from the Python Package Index. + +Python Package Index +-------------------- +If you are familiar with Python install tools (like ``pip`` and +``easy_install``) you can easily install ``ceph-deploy`` like:: + + pip install ceph-deploy + +or:: + + easy_install ceph-deploy + + +It should grab all the dependencies for you and install into the current user's +environment. + +We highly recommend using ``virtualenv`` and installing dependencies in +a contained way. + + +DEB +--- +All new releases of ``ceph-deploy`` are pushed to all ``ceph`` DEB release +repos. + +The DEB release repos are found at:: + + http://ceph.com/debian-{release} + http://ceph.com/debian-testing + +This means, for example, that installing ``ceph-deploy`` from +http://ceph.com/debian-giant will install the same version as from +http://ceph.com/debian-firefly or http://ceph.com/debian-testing. + +RPM +--- +All new releases of ``ceph-deploy`` are pushed to all ``ceph`` RPM release +repos. + +The RPM release repos are found at:: + + http://ceph.com/rpm-{release} + http://ceph.com/rpm-testing + +Make sure you add the proper one for your distribution (i.e. el7 vs rhel7). + +This means, for example, that installing ``ceph-deploy`` from +http://ceph.com/rpm-giant will install the same version as from +http://ceph.com/rpm-firefly or http://ceph.com/rpm-testing. + +bootstrapping +------------- +To get the source tree ready for use, run this once:: + + ./bootstrap + +You can symlink the ``ceph-deploy`` script in this somewhere +convenient (like ``~/bin``), or add the current directory to ``PATH``, +or just always type the full path to ``ceph-deploy``. + + +SSH and Remote Connections +========================== +``ceph-deploy`` will attempt to connect via SSH to hosts when the hostnames do +not match the current host's hostname. For example, if you are connecting to +host ``node1`` it will attempt an SSH connection as long as the current host's +hostname is *not* ``node1``. + +ceph-deploy at a minimum requires that the machine from which the script is +being run can ssh as root without password into each Ceph node. + +To enable this generate a new ssh keypair for the root user with no passphrase +and place the public key (``id_rsa.pub`` or ``id_dsa.pub``) in:: + + /root/.ssh/authorized_keys + +and ensure that the following lines are in the sshd config:: + + PermitRootLogin without-password + PubkeyAuthentication yes + +The machine running ceph-deploy does not need to have the Ceph packages +installed unless it needs to admin the cluster directly using the ``ceph`` +command line tool. + + +usernames +--------- +When not specified the connection will be done with the same username as the +one executing ``ceph-deploy``. This is useful if the same username is shared in +all the nodes but can be cumbersome if that is not the case. + +A way to avoid this is to define the correct usernames to connect with in the +SSH config, but you can also use the ``--username`` flag as well:: + + ceph-deploy --username ceph install node1 + +``ceph-deploy`` then in turn would use ``ceph@node1`` to connect to that host. + +This would be the same expectation for any action that warrants a connection to +a remote host. + + +Managing an existing cluster +============================ + +You can use ceph-deploy to provision nodes for an existing cluster. +To grab a copy of the cluster configuration file (normally +``ceph.conf``):: + + ceph-deploy config pull HOST + +You will usually also want to gather the encryption keys used for that +cluster:: + + ceph-deploy gatherkeys MONHOST + +At this point you can skip the steps below that create a new cluster +(you already have one) and optionally skip installation and/or monitor +creation, depending on what you are trying to accomplish. + + +Creating a new cluster +====================== + +Creating a new configuration +---------------------------- + +To create a new configuration file and secret key, decide what hosts +will run ``ceph-mon``, and run:: + + ceph-deploy new MON [MON..] + +listing the hostnames of the monitors. Each ``MON`` can be + + * a simple hostname. It must be DNS resolvable without the fully + qualified domain name. + * a fully qualified domain name. The hostname is assumed to be the + leading component up to the first ``.``. + * a ``HOST:FQDN`` pair, of both the hostname and a fully qualified + domain name or IP address. For example, ``foo``, + ``foo.example.com``, ``foo:something.example.com``, and + ``foo:1.2.3.4`` are all valid. Note, however, that the hostname + should match that configured on the host ``foo``. + +The above will create a ``ceph.conf`` and ``ceph.mon.keyring`` in your +current directory. + + +Edit initial cluster configuration +---------------------------------- + +You want to review the generated ``ceph.conf`` file and make sure that +the ``mon_host`` setting contains the IP addresses you would like the +monitors to bind to. These are the IPs that clients will initially +contact to authenticate to the cluster, and they need to be reachable +both by external client-facing hosts and internal cluster daemons. + +Installing packages +=================== + +To install the Ceph software on the servers, run:: + + ceph-deploy install HOST [HOST..] + +This installs the current default *stable* release. You can choose a +different release track with command line options, for example to use +a release candidate:: + + ceph-deploy install --testing HOST + +Or to test a development branch:: + + ceph-deploy install --dev=wip-mds-now-works-no-kidding HOST [HOST..] + + +Proxy or Firewall Installs +-------------------------- +If attempting to install behind a firewall or through a proxy you can +use the ``--no-adjust-repos`` that will tell ceph-deploy to skip any changes +to the distro's repository in order to install the packages and it will go +straight to package installation. + +That will allow an environment without internet access to point to *its own +repositories*. This means that those repositories will need to be properly +setup (and mirrored with all the necessary dependencies) before attempting an +install. + +Another alternative is to set the ``wget`` env variables to point to the right +hosts, for example, put following lines into ``/root/.wgetrc`` on each node +(since ceph-deploy runs wget as root):: + + http_proxy=http://host:port + ftp_proxy=http://host:port + https_proxy=http://host:port + + + +Deploying monitors +================== + +To actually deploy ``ceph-mon`` to the hosts you chose, run:: + + ceph-deploy mon create HOST [HOST..] + +Without explicit hosts listed, hosts in ``mon_initial_members`` in the +config file are deployed. That is, the hosts you passed to +``ceph-deploy new`` are the default value here. + +Gather keys +=========== + +To gather authenticate keys (for administering the cluster and +bootstrapping new nodes) to the local directory, run:: + + ceph-deploy gatherkeys HOST [HOST...] + +where ``HOST`` is one of the monitor hosts. + +Once these keys are in the local directory, you can provision new OSDs etc. + + +Deploying OSDs +============== + +To prepare a node for running OSDs, run:: + + ceph-deploy osd create HOST:DISK[:JOURNAL] [HOST:DISK[:JOURNAL] ...] + +After that, the hosts will be running OSDs for the given data disks. +If you specify a raw disk (e.g., ``/dev/sdb``), partitions will be +created and GPT labels will be used to mark and automatically activate +OSD volumes. If an existing partition is specified, the partition +table will not be modified. If you want to destroy the existing +partition table on DISK first, you can include the ``--zap-disk`` +option. + +If there is already a prepared disk or directory that is ready to become an +OSD, you can also do:: + + ceph-deploy osd activate HOST:DIR[:JOURNAL] [...] + +This is useful when you are managing the mounting of volumes yourself. + + +Admin hosts +=========== + +To prepare a host with a ``ceph.conf`` and ``ceph.client.admin.keyring`` +keyring so that it can administer the cluster, run:: + + ceph-deploy admin HOST [HOST ...] + +Forget keys +=========== + +The ``new`` and ``gatherkeys`` put some Ceph authentication keys in keyrings in +the local directory. If you are worried about them being there for security +reasons, run:: + + ceph-deploy forgetkeys + +and they will be removed. If you need them again later to deploy additional +nodes, simply re-run:: + + ceph-deploy gatherkeys HOST [HOST...] + +and they will be retrieved from an existing monitor node. + +Multiple clusters +================= + +All of the above commands take a ``--cluster=NAME`` option, allowing +you to manage multiple clusters conveniently from one workstation. +For example:: + + ceph-deploy --cluster=us-west new + vi us-west.conf + ceph-deploy --cluster=us-west mon + +FAQ +=== + +Before anything +--------------- +Make sure you have the latest version of ``ceph-deploy``. It is actively +developed and releases are coming weekly (on average). The most recent versions +of ``ceph-deploy`` will have a ``--version`` flag you can use, otherwise check +with your package manager and update if there is anything new. + +Why is feature X not implemented? +--------------------------------- +Usually, features are added when/if it is sensible for someone that wants to +get started with ceph and said feature would make sense in that context. If +you believe this is the case and you've read "`what this tool is not`_" and +still think feature ``X`` should exist in ceph-deploy, open a feature request +in the ceph tracker: http://tracker.ceph.com/projects/ceph-deploy/issues + +A command gave me an error, what is going on? +--------------------------------------------- +Most of the commands for ``ceph-deploy`` are meant to be run remotely in a host +that you have configured when creating the initial config. If a given command +is not working as expected try to run the command that failed in the remote +host and assert the behavior there. + +If the behavior in the remote host is the same, then it is probably not +something wrong with ``ceph-deploy`` per-se. Make sure you capture the output +of both the ``ceph-deploy`` output and the output of the command in the remote +host. + +Issues with monitors +-------------------- +If your monitors are not starting, make sure that the ``{hostname}`` you used +when you ran ``ceph-deploy mon create {hostname}`` match the actual ``hostname -s`` +in the remote host. + +Newer versions of ``ceph-deploy`` should warn you if the results are different +but that might prevent the monitors from reaching quorum. + +Developing ceph-deploy +====================== +Now that you have cracked your teeth on Ceph, you might find that you want to +contribute to ceph-deploy. + +Resources +--------- +Bug tracking: http://tracker.ceph.com/projects/ceph-deploy/issues + +Mailing list and IRC info is the same as ceph http://ceph.com/resources/mailing-list-irc/ + +Submitting Patches +------------------ +Please add test cases to cover any code you add. You can test your changes +by running ``tox`` (You will also need ``mock`` and ``pytest`` ) from inside +the git clone + +When creating a commit message please use ``git commit -s`` or otherwise add +``Signed-off-by: Your Name `` to your commit message. + +Patches can then be submitted by a pull request on GitHub. diff --git a/bootstrap b/bootstrap new file mode 100755 index 0000000..c29bdc4 --- /dev/null +++ b/bootstrap @@ -0,0 +1,105 @@ +#!/bin/sh +set -e + +# Use `./bootstrap 3` for Python 3 +python_executable="python$1" + +if ! [ -d virtualenv ]; then + if command -v lsb_release >/dev/null 2>&1; then + if [ "$1" = "2" ]; then + python_package="python" + python_virtualenv_package="python-virtualenv" + else + python_package="python$1" + python_virtualenv_package="python$1-virtualenv" + fi + + case "$(lsb_release --id --short)" in + Ubuntu|Debian) + for package in "$python_package" "$python_virtualenv_package"; do + if [ "$(dpkg --status -- "$package" 2>/dev/null|sed -n 's/^Status: //p')" != "install ok installed" ]; then + # add a space after old values + missing="${missing:+$missing }$package" + fi + done + if [ -n "$missing" ]; then + echo "$0: missing required packages, please install them:" 1>&2 + echo " sudo apt-get install $missing" + exit 1 + fi + ;; + + Arch) + for package in "$python_package" python-virtualenv; do + if ! pacman -Qs -- "$package" >/dev/null 2>&1; then + # add a space after old values + missing="${missing:+$missing }$package" + fi + done + if [ -n "$missing" ]; then + echo "$0: missing required packages, please install them:" 1>&2 + echo " pacman -Sy $missing" + exit 1 + fi + ;; + esac + + case "$(lsb_release --id --short | awk '{print $1}')" in + openSUSE|SUSE) + for package in "$python_package" python-virtualenv; do + if [ "$(rpm -qa "$package" 2>/dev/null)" == "" ]; then + missing="${missing:+$missing }$package" + fi + done + if [ -n "$missing" ]; then + echo "$0: missing required packages, please install them:" 1>&2 + echo " sudo zypper install $missing" + exit 1 + fi + ;; + esac + + fi + + if [ -f /etc/redhat-release ]; then + case "$(cat /etc/redhat-release | awk '{print $1}')" in + CentOS) + for package in python-virtualenv; do + if [ "$(rpm -qa "$package" 2>/dev/null)" == "" ]; then + missing="${missing:+$missing }$package" + fi + done + if [ -n "$missing" ]; then + echo "$0: missing required packages, please install them:" 1>&2 + echo " sudo yum install $missing" + exit 1 + fi + + if [ "${1:-2}" -ge 3 ]; then + if ! command -v "$python_executable" >/dev/null 2>&1; then + echo "$0: missing Python ($python_executable), please install it" + exit 1 + fi + + # Make a temporary virtualenv to get a fresh version of virtualenv + # and use it to make a Python 3 virtualenv, + # because CentOS 7 has buggy old virtualenv (v1.10.1) + # https://github.com/pypa/virtualenv/issues/463 + + virtualenv virtualenv_tmp + virtualenv_tmp/bin/pip install --upgrade setuptools + virtualenv_tmp/bin/pip install --upgrade virtualenv + virtualenv_tmp/bin/virtualenv -p "$python_executable" virtualenv + rm -rf virtualenv_tmp + else + virtualenv virtualenv + fi + ;; + esac + fi +fi + +test -d virtualenv || virtualenv -p "$python_executable" virtualenv +./virtualenv/bin/pip install --upgrade setuptools +./virtualenv/bin/python setup.py develop +test -e ceph-deploy || ln -s virtualenv/bin/ceph-deploy . diff --git a/ceph-deploy.spec b/ceph-deploy.spec new file mode 100644 index 0000000..30f6b60 --- /dev/null +++ b/ceph-deploy.spec @@ -0,0 +1,129 @@ +# +# spec file for package ceph-deploy +# + +%if 0%{?rhel} >= 8 || 0%{?suse_version} >= 1500 +%bcond_with python2 +%bcond_without python3 +%else +%bcond_without python2 +%bcond_with python3 +%endif + +# Exclude ceph-deploy from the rpmbuild shebang check to allow it to run +# under Python 2 and 3. +%global __brp_mangle_shebangs_exclude_from ceph-deploy + +################################################################################# +# common +################################################################################# +Name: ceph-deploy +Version: 2.1.0 +Release: 0 +Summary: Admin and deploy tool for Ceph +License: MIT +Group: System/Filesystems +URL: http://ceph.com/ +Source0: %{name}-%{version}.tar.bz2 +BuildRoot: %{_tmppath}/%{name}-%{version}-build +%if %{with python2} +BuildRequires: python2-devel +BuildRequires: python2-setuptools +BuildRequires: python2-virtualenv +BuildRequires: python2-mock +BuildRequires: python-tox +BuildRequires: git +%if 0%{?suse_version} +BuildRequires: python-pytest +%else +BuildRequires: pytest +%endif +%endif +%if %{with python3} +BuildRequires: python%{python3_pkgversion}-devel +BuildRequires: python%{python3_pkgversion}-setuptools +BuildRequires: python%{python3_pkgversion}-virtualenv +BuildRequires: python%{python3_pkgversion}-mock +BuildRequires: python%{python3_pkgversion}-tox +BuildRequires: python%{python3_pkgversion}-pytest +%endif +BuildArch: noarch +%description +An easy to use admin tool for deploy ceph storage clusters. + + +%if %{with python2} +%package -n python2-%{name} +Summary: %{summary} +Requires: python2-argparse +Requires: python2-configparser +Requires: python2-remoto +%{?python_provide:%python_provide python2-%{name}} +Provides: ceph-deploy +%description -n python2-%{name} +An easy to use admin tool for deploy ceph storage clusters. +%endif + +%if %{with python3} +%package -n python%{python3_pkgversion}-%{name} +Summary: %{summary} +Requires: python%{python3_pkgversion}-remoto +%{?python_provide:%python_provide python%{python3_pkgversion}-%{name}} +Conflicts: ceph-deploy < 2.1.0 +%description -n python%{python3_pkgversion}-%{name} +An easy to use admin tool for deploy ceph storage clusters. +%endif + +################################################################################# +# specific +################################################################################# +%if 0%{defined suse_version} +%py_requires +%endif + +%prep +#%%setup -q -n %%{name} +%setup -q + +%build +# %{?with_python2:%py2_build} +# %{?with_python3:%py3_build} + +%install +%if %{with python2} +%py2_install +mv %{buildroot}%{_bindir}/ceph-deploy %{buildroot}%{_bindir}/ceph-deploy-%{python2_version} +ln -s ./ceph-deploy-%{python2_version} %{buildroot}%{_bindir}/ceph-deploy-2 +ln -s ./ceph-deploy-2 %{buildroot}%{_bindir}/ceph-deploy +%endif +%if %{with python3} +%py3_install +mv %{buildroot}%{_bindir}/ceph-deploy %{buildroot}%{_bindir}/ceph-deploy-%{python3_version} +ln -s ./ceph-deploy-%{python3_version} %{buildroot}%{_bindir}/ceph-deploy-3 +%endif + +%clean +[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf "$RPM_BUILD_ROOT" + +%if %{with python2} +%files -n python2-%{name} +%defattr(-,root,root) +%license LICENSE +%doc README.rst +%{python2_sitelib}/* +%{_bindir}/ceph-deploy +%{_bindir}/ceph-deploy-2 +%{_bindir}/ceph-deploy-%{python2_version} +%endif + +%if %{with python3} +%files -n python3-%{name} +%defattr(-,root,root) +%license LICENSE +%doc README.rst +%{python3_sitelib}/* +%{_bindir}/ceph-deploy-3 +%{_bindir}/ceph-deploy-%{python3_version} +%endif + +%changelog diff --git a/ceph_deploy/__init__.py b/ceph_deploy/__init__.py new file mode 100644 index 0000000..14cde96 --- /dev/null +++ b/ceph_deploy/__init__.py @@ -0,0 +1,3 @@ + +__version__ = '2.1.0' + diff --git a/ceph_deploy/admin.py b/ceph_deploy/admin.py new file mode 100644 index 0000000..7212a1b --- /dev/null +++ b/ceph_deploy/admin.py @@ -0,0 +1,61 @@ +import logging +from ceph_deploy import exc +from ceph_deploy import conf +from ceph_deploy.cliutil import priority +from ceph_deploy import hosts + +LOG = logging.getLogger(__name__) + + +def admin(args): + conf_data = conf.ceph.load_raw(args) + + try: + with open('%s.client.admin.keyring' % args.cluster, 'rb') as f: + keyring = f.read() + except: + raise RuntimeError('%s.client.admin.keyring not found' % + args.cluster) + + errors = 0 + for hostname in args.client: + LOG.debug('Pushing admin keys and conf to %s', hostname) + try: + distro = hosts.get(hostname, username=args.username) + + distro.conn.remote_module.write_conf( + args.cluster, + conf_data, + args.overwrite_conf, + ) + + distro.conn.remote_module.write_file( + '/etc/ceph/%s.client.admin.keyring' % args.cluster, + keyring, + 0o600, + ) + + distro.conn.exit() + + except RuntimeError as e: + LOG.error(e) + errors += 1 + + if errors: + raise exc.GenericError('Failed to configure %d admin hosts' % errors) + + +@priority(70) +def make(parser): + """ + Push configuration and client.admin key to a remote host. + """ + parser.add_argument( + 'client', + metavar='HOST', + nargs='+', + help='host to configure for Ceph administration', + ) + parser.set_defaults( + func=admin, + ) diff --git a/ceph_deploy/cli.py b/ceph_deploy/cli.py new file mode 100644 index 0000000..ccd6df8 --- /dev/null +++ b/ceph_deploy/cli.py @@ -0,0 +1,184 @@ +import pkg_resources +import argparse +import logging +import textwrap +import os +import sys + +import ceph_deploy +from ceph_deploy import exc +from ceph_deploy.util import log +from ceph_deploy.util.decorators import catches + +LOG = logging.getLogger(__name__) + + +__header__ = textwrap.dedent(""" + -^- + / \\ + |O o| ceph-deploy v%s + ).-.( + '/|||\` + | '|` | + '|` + +Full documentation can be found at: http://ceph.com/ceph-deploy/docs +""" % ceph_deploy.__version__) + + +def log_flags(args, logger=None): + logger = logger or LOG + logger.info('ceph-deploy options:') + + for k, v in args.__dict__.items(): + if k.startswith('_'): + continue + logger.info(' %-30s: %s' % (k, v)) + + +def get_parser(): + epilog_text = "See 'ceph-deploy --help' for help on a specific command" + parser = argparse.ArgumentParser( + prog='ceph-deploy', + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Easy Ceph deployment\n\n%s' % __header__, + epilog=epilog_text + ) + verbosity = parser.add_mutually_exclusive_group(required=False) + verbosity.add_argument( + '-v', '--verbose', + action='store_true', dest='verbose', default=False, + help='be more verbose', + ) + verbosity.add_argument( + '-q', '--quiet', + action='store_true', dest='quiet', + help='be less verbose', + ) + parser.add_argument( + '--version', + action='version', + version='%s' % ceph_deploy.__version__, + help='the current installed version of ceph-deploy', + ) + parser.add_argument( + '--username', + help='the username to connect to the remote host', + ) + parser.add_argument( + '--overwrite-conf', + action='store_true', + help='overwrite an existing conf file on remote host (if present)', + ) + parser.add_argument( + '--ceph-conf', + dest='ceph_conf', + help='use (or reuse) a given ceph.conf file', + ) + sub = parser.add_subparsers( + title='commands', + metavar='COMMAND', + help='description', + ) + sub.required = True + entry_points = [ + (ep.name, ep.load()) + for ep in pkg_resources.iter_entry_points('ceph_deploy.cli') + ] + entry_points.sort( + key=lambda name_fn: getattr(name_fn[1], 'priority', 100), + ) + for (name, fn) in entry_points: + p = sub.add_parser( + name, + description=fn.__doc__, + help=fn.__doc__, + ) + if not os.environ.get('CEPH_DEPLOY_TEST'): + p.set_defaults(cd_conf=ceph_deploy.conf.cephdeploy.load()) + + # flag if the default release is being used + p.set_defaults(default_release=False) + fn(p) + p.required = True + parser.set_defaults( + cluster='ceph', + ) + + return parser + + +@catches((KeyboardInterrupt, RuntimeError, exc.DeployError,), handle_all=True) +def _main(args=None, namespace=None): + # Set console logging first with some defaults, to prevent having exceptions + # before hitting logging configuration. The defaults can/will get overridden + # later. + + # Console Logger + sh = logging.StreamHandler() + sh.setFormatter(log.color_format()) + sh.setLevel(logging.WARNING) + + # because we're in a module already, __name__ is not the ancestor of + # the rest of the package; use the root as the logger for everyone + root_logger = logging.getLogger() + + # allow all levels at root_logger, handlers control individual levels + root_logger.setLevel(logging.DEBUG) + root_logger.addHandler(sh) + + parser = get_parser() + if len(sys.argv) < 2: + parser.print_help() + sys.exit() + else: + args = parser.parse_args(args=args, namespace=namespace) + + console_loglevel = logging.DEBUG # start at DEBUG for now + if args.quiet: + console_loglevel = logging.WARNING + if args.verbose: + console_loglevel = logging.DEBUG + + # Console Logger + sh.setLevel(console_loglevel) + + # File Logger + fh = logging.FileHandler('ceph-deploy-{cluster}.log'.format(cluster=args.cluster)) + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter(log.FILE_FORMAT)) + + root_logger.addHandler(fh) + + # Reads from the config file and sets values for the global + # flags and the given sub-command + # the one flag that will never work regardless of the config settings is + # logging because we cannot set it before hand since the logging config is + # not ready yet. This is the earliest we can do. + args = ceph_deploy.conf.cephdeploy.set_overrides(args) + + LOG.info("Invoked (%s): %s" % ( + ceph_deploy.__version__, + ' '.join(sys.argv)) + ) + log_flags(args) + + return args.func(args) + + +def main(args=None, namespace=None): + try: + _main(args=args, namespace=namespace) + finally: + # This block is crucial to avoid having issues with + # Python spitting non-sense thread exceptions. We have already + # handled what we could, so close stderr and stdout. + if not os.environ.get('CEPH_DEPLOY_TEST'): + try: + sys.stdout.close() + except: + pass + try: + sys.stderr.close() + except: + pass diff --git a/ceph_deploy/cliutil.py b/ceph_deploy/cliutil.py new file mode 100644 index 0000000..d273f31 --- /dev/null +++ b/ceph_deploy/cliutil.py @@ -0,0 +1,8 @@ +def priority(num): + """ + Decorator to add a `priority` attribute to the function. + """ + def add_priority(fn): + fn.priority = num + return fn + return add_priority diff --git a/ceph_deploy/conf/__init__.py b/ceph_deploy/conf/__init__.py new file mode 100644 index 0000000..8599a6a --- /dev/null +++ b/ceph_deploy/conf/__init__.py @@ -0,0 +1,2 @@ +from . import ceph # noqa +from . import cephdeploy # noqa diff --git a/ceph_deploy/conf/ceph.py b/ceph_deploy/conf/ceph.py new file mode 100644 index 0000000..b9cefe1 --- /dev/null +++ b/ceph_deploy/conf/ceph.py @@ -0,0 +1,102 @@ +import configparser +import contextlib + +from ceph_deploy import exc + + +class _TrimIndentFile(object): + def __init__(self, fp): + self.fp = fp + + def readline(self): + line = self.fp.readline() + return line.lstrip(' \t') + + def __iter__(self): + return iter(self.readline, '') + +class CephConf(configparser.RawConfigParser): + def __init__(self): + # super() cannot be used with an old-style class + configparser.RawConfigParser.__init__(self, strict=False) + + def optionxform(self, s): + s = s.replace('_', ' ') + s = '_'.join(s.split()) + return s + + def safe_get(self, section, key): + """ + Attempt to get a configuration value from a certain section + in a ``cfg`` object but returning None if not found. Avoids the need + to be doing try/except {ConfigParser Exceptions} every time. + """ + try: + #Use full parent function so we can replace it in the class + # if desired + return configparser.RawConfigParser.get(self, section, key) + except (configparser.NoSectionError, + configparser.NoOptionError): + return None + + +def parse(fp): + cfg = CephConf() + ifp = _TrimIndentFile(fp) + cfg.read_file(ifp) + return cfg + + +def load(args): + """ + :param args: Will be used to infer the proper configuration name, or + if args.ceph_conf is passed in, that will take precedence + """ + path = args.ceph_conf or '{cluster}.conf'.format(cluster=args.cluster) + + try: + f = open(path) + except IOError as e: + raise exc.ConfigError( + "%s; has `ceph-deploy new` been run in this directory?" % e + ) + else: + with contextlib.closing(f): + return parse(f) + + +def load_raw(args): + """ + Read the actual file *as is* without parsing/modifiying it + so that it can be written maintaining its same properties. + + :param args: Will be used to infer the proper configuration name + :paran path: alternatively, use a path for any configuration file loading + """ + path = args.ceph_conf or '{cluster}.conf'.format(cluster=args.cluster) + try: + with open(path) as ceph_conf: + return ceph_conf.read() + except (IOError, OSError) as e: + raise exc.ConfigError( + "%s; has `ceph-deploy new` been run in this directory?" % e + ) + + +def write_conf(cluster, conf, overwrite): + """ write cluster configuration to /etc/ceph/{cluster}.conf """ + import os + + path = '/etc/ceph/{cluster}.conf'.format(cluster=cluster) + tmp = '{path}.{pid}.tmp'.format(path=path, pid=os.getpid()) + + if os.path.exists(path): + with open(path) as f: + old = f.read() + if old != conf and not overwrite: + raise RuntimeError('config file %s exists with different content; use --overwrite-conf to overwrite' % path) + with open(tmp, 'w') as f: + f.write(conf) + f.flush() + os.fsync(f) + os.rename(tmp, path) diff --git a/ceph_deploy/conf/cephdeploy.py b/ceph_deploy/conf/cephdeploy.py new file mode 100644 index 0000000..22e768b --- /dev/null +++ b/ceph_deploy/conf/cephdeploy.py @@ -0,0 +1,216 @@ +import configparser +import logging +import os +from os import path +import re + +from ceph_deploy.util.paths import gpg + +logger = logging.getLogger('ceph_deploy.conf') + +cd_conf_template = """ +# +# ceph-deploy configuration file +# + +[ceph-deploy-global] +# Overrides for some of ceph-deploy's global flags, like verbosity or cluster +# name + +[ceph-deploy-install] +# Overrides for some of ceph-deploy's install flags, like version of ceph to +# install + + +# +# Repositories section +# + +# yum repos: +# [myrepo] +# baseurl = http://gitbuilder.ceph.com/ceph-rpm-centos7-x86_64-basic/ref/hammer +# gpgurl = {gpg_url_autobuild} +# default = True +# extra-repos = cephrepo # will install the cephrepo file too +# +# [cephrepo] +# name=ceph repo noarch packages +# baseurl=http://download.ceph.com/rpm-hammer/el6/noarch +# enabled=1 +# gpgcheck=1 +# type=rpm-md +# gpgkey={gpg_url_release} + +# apt repos: +# [myrepo] +# baseurl = http://gitbuilder.ceph.com/ceph-deb-trusty-x86_64-basic/ref/hammer +# gpgurl = {gpg_url_autobuild} +# default = True +# extra-repos = cephrepo # will install the cephrepo file too +# +# [cephrepo] +# baseurl=http://download.ceph.com/debian-hammer +# gpgkey={gpg_url_release} +""".format(gpg_url_release=gpg.url('release'), + gpg_url_autobuild=gpg.url('autobuild')) + + +def location(): + """ + Find and return the location of the ceph-deploy configuration file. If this + file does not exist, create one in a default location. + """ + return _locate_or_create() + + +def load(): + parser = Conf() + parser.read(location()) + return parser + + +def _locate_or_create(): + home_config = path.expanduser('~/.cephdeploy.conf') + # With order of importance + locations = [ + path.join(os.getcwd(), 'cephdeploy.conf'), + home_config, + ] + + for location in locations: + if path.exists(location): + logger.debug('found configuration file at: %s' % location) + return location + logger.info('could not find configuration file, will create one in $HOME') + create_stub(home_config) + return home_config + + +def create_stub(_path=None): + _path = _path or path.expanduser('~/.cephdeploy.conf') + logger.debug('creating new configuration file: %s' % _path) + with open(_path, 'w') as cd_conf: + cd_conf.write(cd_conf_template) + + +def set_overrides(args, _conf=None): + """ + Read the configuration file and look for ceph-deploy sections + to set flags/defaults from the values found. This will alter the + ``args`` object that is created by argparse. + """ + # Get the subcommand name to avoid overwritting values from other + # subcommands that are not going to be used + subcommand = args.func.__name__ + command_section = 'ceph-deploy-%s' % subcommand + conf = _conf or load() + + for section_name in conf.sections(): + if section_name in ['ceph-deploy-global', command_section]: + override_subcommand( + section_name, + conf.items(section_name), + args + ) + return args + + +def override_subcommand(section_name, section_items, args): + """ + Given a specific section in the configuration file that maps to + a subcommand (except for the global section) read all the keys that are + actual argument flags and slap the values for that one subcommand. + + Return the altered ``args`` object at the end. + """ + # XXX We are not coercing here any int-like values, so if ArgParse + # does that in the CLI we are totally non-compliant with that expectation + # but we will try and infer a few boolean values + + # acceptable boolean states for flags + _boolean_states = {'yes': True, 'true': True, 'on': True, + 'no': False, 'false': False, 'off': False} + + for k, v, in section_items: + # get the lower case value of `v`, fallback to the booleanized + # (original) value of `v` + try: + normalized_value = v.lower() + except AttributeError: + # probably not a string object that has .lower + normalized_value = v + value = _boolean_states.get(normalized_value, v) + setattr(args, k, value) + return args + + +class Conf(configparser.ConfigParser): + """ + Subclasses from ConfigParser to give a few helpers for the ceph-deploy + configuration. Specifically, it addresses the need to work with custom + sections that signal the usage of custom repositories. + """ + + reserved_sections = ['ceph-deploy-global', 'ceph-deploy-install'] + + def get_safe(self, section, key, default=None): + """ + Attempt to get a configuration value from a certain section + in a ``cfg`` object but returning None if not found. Avoids the need + to be doing try/except {ConfigParser Exceptions} every time. + """ + try: + return self.get(section, key) + except (configparser.NoSectionError, configparser.NoOptionError): + return default + + def get_repos(self): + """ + Return all the repo sections from the config, excluding the ceph-deploy + reserved sections. + """ + return [ + section for section in self.sections() + if section not in self.reserved_sections + ] + + @property + def has_repos(self): + """ + boolean to reflect having (or not) any repository sections + """ + for section in self.sections(): + if section not in self.reserved_sections: + return True + return False + + def get_list(self, section, key): + """ + Assumes that the value for a given key is going to be a list + separated by commas. It gets rid of trailing comments. + If just one item is present it returns a list with a single item, if no + key is found an empty list is returned. + """ + value = self.get_safe(section, key, []) + if value == []: + return value + + # strip comments + value = re.split(r'\s+#', value)[0] + + # split on commas + value = value.split(',') + + # strip spaces + return [x.strip() for x in value] + + def get_default_repo(self): + """ + Go through all the repositories defined in the config file and search + for a truthy value for the ``default`` key. If there isn't any return + None. + """ + for repo in self.get_repos(): + if self.get_safe(repo, 'default') and self.getboolean(repo, 'default'): + return repo + return False diff --git a/ceph_deploy/config.py b/ceph_deploy/config.py new file mode 100644 index 0000000..1f85ed7 --- /dev/null +++ b/ceph_deploy/config.py @@ -0,0 +1,111 @@ +import logging +import os.path + +from ceph_deploy import exc +from ceph_deploy import conf +from ceph_deploy.cliutil import priority +from ceph_deploy import hosts + +LOG = logging.getLogger(__name__) + + +def config_push(args): + conf_data = conf.ceph.load_raw(args) + + errors = 0 + for hostname in args.client: + LOG.debug('Pushing config to %s', hostname) + try: + distro = hosts.get(hostname, username=args.username) + + distro.conn.remote_module.write_conf( + args.cluster, + conf_data, + args.overwrite_conf, + ) + + distro.conn.exit() + + except RuntimeError as e: + LOG.error(e) + errors += 1 + + if errors: + raise exc.GenericError('Failed to config %d hosts' % errors) + + +def config_pull(args): + + topath = '{cluster}.conf'.format(cluster=args.cluster) + frompath = '/etc/ceph/{cluster}.conf'.format(cluster=args.cluster) + + errors = 0 + for hostname in args.client: + try: + LOG.debug('Checking %s for %s', hostname, frompath) + distro = hosts.get(hostname, username=args.username) + conf_file_contents = distro.conn.remote_module.get_file(frompath) + + if conf_file_contents is not None: + LOG.debug('Got %s from %s', frompath, hostname) + if os.path.exists(topath): + with open(topath, 'rb') as f: + existing = f.read() + if existing != conf_file_contents and not args.overwrite_conf: + LOG.error('local config file %s exists with different content; use --overwrite-conf to overwrite' % topath) + raise + + with open(topath, 'wb') as f: + f.write(conf_file_contents) + return + distro.conn.exit() + LOG.debug('Empty or missing %s on %s', frompath, hostname) + except: + LOG.error('Unable to pull %s from %s', frompath, hostname) + finally: + errors += 1 + + raise exc.GenericError('Failed to fetch config from %d hosts' % errors) + + +def config(args): + if args.subcommand == 'push': + config_push(args) + elif args.subcommand == 'pull': + config_pull(args) + else: + LOG.error('subcommand %s not implemented', args.subcommand) + + +@priority(70) +def make(parser): + """ + Copy ceph.conf to/from remote host(s) + """ + config_parser = parser.add_subparsers(dest='subcommand') + config_parser.required = True + + config_push = config_parser.add_parser( + 'push', + help='push Ceph config file to one or more remote hosts' + ) + config_push.add_argument( + 'client', + metavar='HOST', + nargs='+', + help='host(s) to push the config file to', + ) + + config_pull = config_parser.add_parser( + 'pull', + help='pull Ceph config file from one or more remote hosts' + ) + config_pull.add_argument( + 'client', + metavar='HOST', + nargs='+', + help='host(s) to pull the config file from', + ) + parser.set_defaults( + func=config, + ) diff --git a/ceph_deploy/connection.py b/ceph_deploy/connection.py new file mode 100644 index 0000000..fd71983 --- /dev/null +++ b/ceph_deploy/connection.py @@ -0,0 +1,44 @@ +import socket +from ceph_deploy.lib import remoto + + +def get_connection(hostname, username, logger, threads=5, use_sudo=None, detect_sudo=True): + """ + A very simple helper, meant to return a connection + that will know about the need to use sudo. + """ + if username: + hostname = "%s@%s" % (username, hostname) + try: + conn = remoto.Connection( + hostname, + logger=logger, + threads=threads, + detect_sudo=detect_sudo, + ) + + # Set a timeout value in seconds to disconnect and move on + # if no data is sent back. + conn.global_timeout = 300 + logger.debug("connected to host: %s " % hostname) + return conn + + except Exception as error: + msg = "connecting to host: %s " % hostname + errors = "resulted in errors: %s %s" % (error.__class__.__name__, error) + raise RuntimeError(msg + errors) + + +def get_local_connection(logger, use_sudo=False): + """ + Helper for local connections that are sometimes needed to operate + on local hosts + """ + return get_connection( + socket.gethostname(), # cannot rely on 'localhost' here + None, + logger=logger, + threads=1, + use_sudo=use_sudo, + detect_sudo=False + ) diff --git a/ceph_deploy/exc.py b/ceph_deploy/exc.py new file mode 100644 index 0000000..064fb9b --- /dev/null +++ b/ceph_deploy/exc.py @@ -0,0 +1,127 @@ +class DeployError(Exception): + """ + Unknown deploy error + """ + + def __str__(self): + doc = self.__doc__.strip() + return ': '.join([doc] + [str(a) for a in self.args]) + + +class UnableToResolveError(DeployError): + """ + Unable to resolve host + """ + + +class ClusterExistsError(DeployError): + """ + Cluster config exists already + """ + + +class ConfigError(DeployError): + """ + Cannot load config + """ + + +class NeedHostError(DeployError): + """ + No hosts specified to deploy to. + """ + + +class NeedMonError(DeployError): + """ + Cannot find nodes with ceph-mon. + """ + + +class NeedDiskError(DeployError): + """ + Must supply disk/path argument + """ + + +class UnsupportedPlatform(DeployError): + """ + Platform is not supported + """ + def __init__(self, distro, codename, release): + self.distro = distro + self.codename = codename + self.release = release + + def __str__(self): + return '{doc}: {distro} {codename} {release}'.format( + doc=self.__doc__.strip(), + distro=self.distro, + codename=self.codename, + release=self.release, + ) + + +class ExecutableNotFound(DeployError): + """ + Could not locate executable + """ + def __init__(self, executable, host): + self.executable = executable + self.host = host + + def __str__(self): + return "{doc} '{executable}' make sure it is installed and available on {host}".format( + doc=self.__doc__.strip(), + executable=self.executable, + host=self.host, + ) + + +class MissingPackageError(DeployError): + """ + A required package or command is missing + """ + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + + +class GenericError(DeployError): + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + + +class ClusterNameError(DeployError): + """ + Problem encountered with custom cluster name + """ + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + + +class KeyNotFoundError(DeployError): + """ + Could not find keyring file + """ + def __init__(self, keyring, hosts): + self.keyring = keyring + self.hosts = hosts + + def __str__(self): + return '{doc}: {keys}'.format( + doc=self.__doc__.strip(), + keys=', '.join( + [self.keyring.format(hostname=host) + + " on host {hostname}".format(hostname=host) + for host in self.hosts] + ) + ) diff --git a/ceph_deploy/forgetkeys.py b/ceph_deploy/forgetkeys.py new file mode 100644 index 0000000..faa376e --- /dev/null +++ b/ceph_deploy/forgetkeys.py @@ -0,0 +1,37 @@ +import logging +import errno + +from .cliutil import priority + + +LOG = logging.getLogger(__name__) + + +def forgetkeys(args): + import os + for f in [ + 'mon', + 'client.admin', + 'bootstrap-osd', + 'bootstrap-mds', + 'bootstrap-rgw', + ]: + try: + os.unlink('{cluster}.{what}.keyring'.format( + cluster=args.cluster, + what=f, + )) + except OSError as e: + if e.errno == errno.ENOENT: + pass + else: + raise + +@priority(100) +def make(parser): + """ + Remove authentication keys from the local directory. + """ + parser.set_defaults( + func=forgetkeys, + ) diff --git a/ceph_deploy/gatherkeys.py b/ceph_deploy/gatherkeys.py new file mode 100644 index 0000000..8d0bd60 --- /dev/null +++ b/ceph_deploy/gatherkeys.py @@ -0,0 +1,285 @@ +import errno +import os.path +import logging +import json +import tempfile +import shutil +import time + +from ceph_deploy import hosts +from ceph_deploy.cliutil import priority +from ceph_deploy.lib import remoto +from ceph_deploy.util import as_string +import ceph_deploy.util.paths.mon + +LOG = logging.getLogger(__name__) + + +def _keyring_equivalent(keyring_one, keyring_two): + """ + Check two keyrings are identical + """ + def keyring_extract_key(file_path): + """ + Cephx keyring files may or may not have white space before some lines. + They may have some values in quotes, so a safe way to compare is to + extract the key. + """ + with open(file_path) as f: + for line in f: + content = line.strip() + if len(content) == 0: + continue + split_line = content.split('=') + if split_line[0].strip() == 'key': + return "=".join(split_line[1:]).strip() + raise RuntimeError("File '%s' is not a keyring" % file_path) + key_one = keyring_extract_key(keyring_one) + key_two = keyring_extract_key(keyring_two) + return key_one == key_two + + +def keytype_path_to(args, keytype): + """ + Get the local filename for a keyring type + """ + if keytype == "admin": + return '{cluster}.client.admin.keyring'.format( + cluster=args.cluster) + if keytype == "mon": + return '{cluster}.mon.keyring'.format( + cluster=args.cluster) + return '{cluster}.bootstrap-{what}.keyring'.format( + cluster=args.cluster, + what=keytype) + + +def keytype_identity(keytype): + """ + Get the keyring identity from keyring type. + + This is used in authentication with keyrings and generating keyrings. + """ + ident_dict = { + 'admin' : 'client.admin', + 'mds' : 'client.bootstrap-mds', + 'mgr' : 'client.bootstrap-mgr', + 'osd' : 'client.bootstrap-osd', + 'rgw' : 'client.bootstrap-rgw', + 'mon' : 'mon.' + } + return ident_dict.get(keytype, None) + + +def keytype_capabilities(keytype): + """ + Get the capabilities of a keyring from keyring type. + """ + cap_dict = { + 'admin' : [ + 'osd', 'allow *', + 'mds', 'allow *', + 'mon', 'allow *', + 'mgr', 'allow *' + ], + 'mds' : [ + 'mon', 'allow profile bootstrap-mds' + ], + 'mgr' : [ + 'mon', 'allow profile bootstrap-mgr' + ], + 'osd' : [ + 'mon', 'allow profile bootstrap-osd' + ], + 'rgw': [ + 'mon', 'allow profile bootstrap-rgw' + ] + } + return cap_dict.get(keytype, None) + + +def gatherkeys_missing(args, distro, rlogger, keypath, keytype, dest_dir): + """ + Get or create the keyring from the mon using the mon keyring by keytype and + copy to dest_dir + """ + args_prefix = [ + '/usr/bin/ceph', + '--connect-timeout=25', + '--cluster={cluster}'.format( + cluster=args.cluster), + '--name', 'mon.', + '--keyring={keypath}'.format( + keypath=keypath), + ] + + identity = keytype_identity(keytype) + if identity is None: + raise RuntimeError('Could not find identity for keytype:%s' % keytype) + capabilites = keytype_capabilities(keytype) + if capabilites is None: + raise RuntimeError('Could not find capabilites for keytype:%s' % keytype) + + # First try getting the key if it already exists, to handle the case where + # it exists but doesn't match the caps we would pass into get-or-create. + # This is the same behvaior as in newer ceph-create-keys + out, err, code = remoto.process.check( + distro.conn, + args_prefix + ['auth', 'get', identity] + ) + if code == errno.ENOENT: + out, err, code = remoto.process.check( + distro.conn, + args_prefix + ['auth', 'get-or-create', identity] + capabilites + ) + if code != 0: + rlogger.error( + '"ceph auth get-or-create for keytype %s returned %s', + keytype, code + ) + for line in err: + rlogger.debug(line) + return False + keyring_name_local = keytype_path_to(args, keytype) + keyring_path_local = os.path.join(dest_dir, keyring_name_local) + with open(keyring_path_local, 'w') as f: + for line in out: + f.write(as_string(line) + '\n') + return True + + +def gatherkeys_with_mon(args, host, dest_dir): + """ + Connect to mon and gather keys if mon is in quorum. + """ + distro = hosts.get(host, username=args.username) + remote_hostname = distro.conn.remote_module.shortname() + dir_keytype_mon = ceph_deploy.util.paths.mon.path(args.cluster, remote_hostname) + path_keytype_mon = "%s/keyring" % (dir_keytype_mon) + mon_key = distro.conn.remote_module.get_file(path_keytype_mon) + if mon_key is None: + LOG.warning("No mon key found in host: %s", host) + return False + mon_name_local = keytype_path_to(args, "mon") + mon_path_local = os.path.join(dest_dir, mon_name_local) + with open(mon_path_local, 'w') as f: + f.write(as_string(mon_key)) + rlogger = logging.getLogger(host) + path_asok = ceph_deploy.util.paths.mon.asok(args.cluster, remote_hostname) + out, err, code = remoto.process.check( + distro.conn, + [ + "/usr/bin/ceph", + "--connect-timeout=25", + "--cluster={cluster}".format( + cluster=args.cluster), + "--admin-daemon={asok}".format( + asok=path_asok), + "mon_status" + ] + ) + if code != 0: + rlogger.error('"ceph mon_status %s" returned %s', host, code) + for line in err: + rlogger.debug(line) + return False + try: + mon_status = json.loads(''.join(out)) + except ValueError: + rlogger.error('"ceph mon_status %s" output was not json', host) + for line in out: + rlogger.error(line) + return False + mon_number = None + mon_map = mon_status.get('monmap') + if mon_map is None: + rlogger.error("could not find mon map for mons on '%s'", host) + return False + mon_quorum = mon_status.get('quorum') + if mon_quorum is None: + rlogger.error("could not find quorum for mons on '%s'" , host) + return False + mon_map_mons = mon_map.get('mons') + if mon_map_mons is None: + rlogger.error("could not find mons in monmap on '%s'", host) + return False + for mon in mon_map_mons: + if mon.get('name') == remote_hostname: + mon_number = mon.get('rank') + break + if mon_number is None: + rlogger.error("could not find '%s' in monmap", remote_hostname) + return False + if not mon_number in mon_quorum: + rlogger.error("Not yet quorum for '%s'", host) + return False + for keytype in ["admin", "mds", "mgr", "osd", "rgw"]: + if not gatherkeys_missing(args, distro, rlogger, path_keytype_mon, keytype, dest_dir): + # We will return failure if we fail to gather any key + rlogger.error("Failed to return '%s' key from host %s", keytype, host) + return False + return True + + +def gatherkeys(args): + """ + Gather keys from any mon and store in current working directory. + + Backs up keys from previous installs and stores new keys. + """ + oldmask = os.umask(0o77) + try: + try: + tmpd = tempfile.mkdtemp() + LOG.info("Storing keys in temp directory %s", tmpd) + sucess = False + for host in args.mon: + sucess = gatherkeys_with_mon(args, host, tmpd) + if sucess: + break + if not sucess: + LOG.error("Failed to connect to host:%s" ,', '.join(args.mon)) + raise RuntimeError('Failed to connect any mon') + had_error = False + date_string = time.strftime("%Y%m%d%H%M%S") + for keytype in ["admin", "mds", "mgr", "mon", "osd", "rgw"]: + filename = keytype_path_to(args, keytype) + tmp_path = os.path.join(tmpd, filename) + if not os.path.exists(tmp_path): + LOG.error("No key retrived for '%s'" , keytype) + had_error = True + continue + if not os.path.exists(filename): + LOG.info("Storing %s" % (filename)) + shutil.move(tmp_path, filename) + continue + if _keyring_equivalent(tmp_path, filename): + LOG.info("keyring '%s' already exists" , filename) + continue + backup_keyring = "%s-%s" % (filename, date_string) + LOG.info("Replacing '%s' and backing up old key as '%s'", filename, backup_keyring) + shutil.copy(filename, backup_keyring) + shutil.move(tmp_path, filename) + if had_error: + raise RuntimeError('Failed to get all key types') + finally: + LOG.info("Destroy temp directory %s" %(tmpd)) + shutil.rmtree(tmpd) + finally: + os.umask(oldmask) + + +@priority(40) +def make(parser): + """ + Gather authentication keys for provisioning new nodes. + """ + parser.add_argument( + 'mon', + metavar='HOST', + nargs='+', + help='monitor host to pull keys from', + ) + parser.set_defaults( + func=gatherkeys, + ) diff --git a/ceph_deploy/hosts/__init__.py b/ceph_deploy/hosts/__init__.py new file mode 100644 index 0000000..a2a79be --- /dev/null +++ b/ceph_deploy/hosts/__init__.py @@ -0,0 +1,154 @@ +""" +We deal (mostly) with remote hosts. To avoid special casing each different +commands (e.g. using `yum` as opposed to `apt`) we can make a one time call to +that remote host and set all the special cases for running commands depending +on the type of distribution/version we are dealing with. +""" +import logging +from ceph_deploy import exc +from ceph_deploy.util import versions +from ceph_deploy.hosts import debian, centos, fedora, suse, remotes, rhel, arch, alt, clear +from ceph_deploy.connection import get_connection + +logger = logging.getLogger() + + +def get(hostname, + username=None, + fallback=None, + detect_sudo=True, + use_rhceph=False, + callbacks=None): + """ + Retrieve the module that matches the distribution of a ``hostname``. This + function will connect to that host and retrieve the distribution + information, then return the appropriate module and slap a few attributes + to that module defining the information it found from the hostname. + + For example, if host ``node1.example.com`` is an Ubuntu server, the + ``debian`` module would be returned and the following would be set:: + + module.name = 'ubuntu' + module.release = '12.04' + module.codename = 'precise' + + :param hostname: A hostname that is reachable/resolvable over the network + :param fallback: Optional fallback to use if no supported distro is found + :param use_rhceph: Whether or not to install RH Ceph on a RHEL machine or + the community distro. Changes what host module is + returned for RHEL. + :params callbacks: A list of callables that accept one argument (the actual + module that contains the connection) that will be + called, in order at the end of the instantiation of the + module. + """ + conn = get_connection( + hostname, + username=username, + logger=logging.getLogger(hostname), + detect_sudo=detect_sudo + ) + try: + conn.import_module(remotes) + except IOError as error: + if 'already closed' in getattr(error, 'message', ''): + raise RuntimeError('remote connection got closed, ensure ``requiretty`` is disabled for %s' % hostname) + distro_name, release, codename = conn.remote_module.platform_information() + if not codename or not _get_distro(distro_name): + raise exc.UnsupportedPlatform( + distro=distro_name, + codename=codename, + release=release) + + machine_type = conn.remote_module.machine_type() + module = _get_distro(distro_name, use_rhceph=use_rhceph) + module.name = distro_name + module.normalized_name = _normalized_distro_name(distro_name) + module.normalized_release = _normalized_release(release) + module.distro = module.normalized_name + module.is_el = module.normalized_name in ['redhat', 'centos', 'fedora', 'scientific', 'oracle', 'virtuozzo'] + module.is_rpm = module.normalized_name in ['redhat', 'centos', + 'fedora', 'scientific', 'suse', 'oracle', 'virtuozzo', 'alt'] + module.is_deb = module.normalized_name in ['debian', 'ubuntu'] + module.is_pkgtarxz = module.normalized_name in ['arch'] + module.is_swupd = module.normalized_name in ['clear'] + module.release = release + module.codename = codename + module.conn = conn + module.machine_type = machine_type + module.init = module.choose_init(module) + module.packager = module.get_packager(module) + # execute each callback if any + if callbacks: + for c in callbacks: + c(module) + return module + + +def _get_distro(distro, fallback=None, use_rhceph=False): + if not distro: + return + + distro = _normalized_distro_name(distro) + distributions = { + 'debian': debian, + 'ubuntu': debian, + 'centos': centos, + 'scientific': centos, + 'oracle': centos, + 'redhat': centos, + 'fedora': fedora, + 'openeuler': centos, + 'suse': suse, + 'virtuozzo': centos, + 'arch': arch, + 'alt': alt, + 'clear': clear + } + + if distro == 'redhat' and use_rhceph: + return rhel + else: + return distributions.get(distro) or _get_distro(fallback) + + +def _normalized_distro_name(distro): + distro = distro.lower() + if distro.startswith(('redhat', 'red hat')): + return 'redhat' + elif distro.startswith(('scientific', 'scientific linux')): + return 'scientific' + elif distro.startswith('oracle'): + return 'oracle' + elif distro.startswith(('suse', 'opensuse', 'sles')): + return 'suse' + elif distro.startswith(('centos', 'euleros', 'openeuler')): + return 'centos' + elif distro.startswith(('linuxmint', 'kylin')): + return 'ubuntu' + elif distro.startswith('virtuozzo'): + return 'virtuozzo' + elif distro.startswith('arch'): + return 'arch' + elif distro.startswith(('alt', 'altlinux', 'basealt', 'alt linux')): + return 'alt' + elif distro.startswith('clear'): + return 'clear' + return distro + + +def _normalized_release(release): + """ + A normalizer function to make sense of distro + release versions. + + Returns an object with: major, minor, patch, and garbage + + These attributes can be accessed as ints with prefixed "int" + attribute names, for example: + + normalized_version.int_major + """ + # TODO: at some point deprecate this function so that we just + # use this class directly (and update every test that calls it + return versions.NormalizedVersion(release) diff --git a/ceph_deploy/hosts/alt/__init__.py b/ceph_deploy/hosts/alt/__init__.py new file mode 100644 index 0000000..a4e53f3 --- /dev/null +++ b/ceph_deploy/hosts/alt/__init__.py @@ -0,0 +1,30 @@ +from . import mon # noqa +from .install import install # noqa +from .uninstall import uninstall # noqa +from ceph_deploy.util import pkg_managers +from ceph_deploy.util.system import is_systemd + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + + +def choose_init(module): + """ + Select a init system + + Returns the name of a init system (systemd, sysvinit ...). + """ + + + if is_systemd(module.conn): + return 'systemd' + + return 'sysvinit' + + +def get_packager(module): + return pkg_managers.AptRpm(module) diff --git a/ceph_deploy/hosts/alt/install.py b/ceph_deploy/hosts/alt/install.py new file mode 100644 index 0000000..e795b1b --- /dev/null +++ b/ceph_deploy/hosts/alt/install.py @@ -0,0 +1,43 @@ +from ceph_deploy.hosts.centos.install import repo_install, mirror_install # noqa +from ceph_deploy.hosts.common import map_components +from ceph_deploy.util.system import enable_service, start_service + + +NON_SPLIT_PACKAGES = [ + 'ceph-osd', + 'ceph-mds', + 'ceph-mon', + 'ceph-mgr', +] + +SYSTEMD_UNITS = [ + 'ceph.target', + 'ceph-mds.target', + 'ceph-mon.target', + 'ceph-mgr.target', + 'ceph-osd.target', +] +SYSTEMD_UNITS_SKIP_START = [ + 'ceph-mgr.target', + 'ceph-mon.target', +] +SYSTEMD_UNITS_SKIP_ENABLE = [ +] + + +def install(distro, version_kind, version, adjust_repos, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.pop('components', []) + ) + + if packages: + distro.packager.clean() + distro.packager.install(packages) + + # Start and enable services + for unit in SYSTEMD_UNITS: + if unit not in SYSTEMD_UNITS_SKIP_START: + start_service(distro.conn, unit) + if unit not in SYSTEMD_UNITS_SKIP_ENABLE: + enable_service(distro.conn, unit) diff --git a/ceph_deploy/hosts/alt/mon/__init__.py b/ceph_deploy/hosts/alt/mon/__init__.py new file mode 100644 index 0000000..f266fb0 --- /dev/null +++ b/ceph_deploy/hosts/alt/mon/__init__.py @@ -0,0 +1,2 @@ +from ceph_deploy.hosts.common import mon_add as add # noqa +from ceph_deploy.hosts.common import mon_create as create # noqa diff --git a/ceph_deploy/hosts/alt/uninstall.py b/ceph_deploy/hosts/alt/uninstall.py new file mode 100644 index 0000000..00c808a --- /dev/null +++ b/ceph_deploy/hosts/alt/uninstall.py @@ -0,0 +1,11 @@ +def uninstall(distro, purge=False): + packages = [ + 'ceph-common', + 'ceph-base', + 'ceph-radosgw', + 'python-module-cephfs', + 'python-module-rados', + 'python-module-rbd', + 'python-module-rgw', + ] + distro.packager.remove(packages) diff --git a/ceph_deploy/hosts/arch/__init__.py b/ceph_deploy/hosts/arch/__init__.py new file mode 100644 index 0000000..2e8c0ab --- /dev/null +++ b/ceph_deploy/hosts/arch/__init__.py @@ -0,0 +1,26 @@ +from . import mon # noqa +from ceph_deploy.hosts.centos.install import repo_install # noqa +from .install import install, mirror_install # noqa +from .uninstall import uninstall # noqa +from ceph_deploy.util import pkg_managers + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + + +def choose_init(module): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + + return 'systemd' + + +def get_packager(module): + return pkg_managers.Pacman(module) diff --git a/ceph_deploy/hosts/arch/install.py b/ceph_deploy/hosts/arch/install.py new file mode 100644 index 0000000..7625298 --- /dev/null +++ b/ceph_deploy/hosts/arch/install.py @@ -0,0 +1,49 @@ +from ceph_deploy.hosts.centos.install import repo_install, mirror_install # noqa +from ceph_deploy.hosts.common import map_components +from ceph_deploy.util.system import enable_service, start_service + + +NON_SPLIT_PACKAGES = [ + 'ceph-osd', + 'ceph-radosgw', + 'ceph-mds', + 'ceph-mon', + 'ceph-mgr', + 'ceph-common', + 'ceph-test' +] + +SYSTEMD_UNITS = [ + 'ceph.target', + 'ceph-radosgw.target', + 'ceph-rbd-mirror.target', + 'ceph-fuse.target', + 'ceph-mds.target', + 'ceph-mon.target', + 'ceph-mgr.target', + 'ceph-osd.target', +] +SYSTEMD_UNITS_SKIP_START = [ + 'ceph-mgr.target', + 'ceph-mon.target', +] +SYSTEMD_UNITS_SKIP_ENABLE = [ +] + + +def install(distro, version_kind, version, adjust_repos, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.pop('components', []) + ) + + distro.packager.install( + packages + ) + + # Start and enable services + for unit in SYSTEMD_UNITS: + if unit not in SYSTEMD_UNITS_SKIP_START: + start_service(distro.conn, unit) + if unit not in SYSTEMD_UNITS_SKIP_ENABLE: + enable_service(distro.conn, unit) diff --git a/ceph_deploy/hosts/arch/mon/__init__.py b/ceph_deploy/hosts/arch/mon/__init__.py new file mode 100644 index 0000000..f266fb0 --- /dev/null +++ b/ceph_deploy/hosts/arch/mon/__init__.py @@ -0,0 +1,2 @@ +from ceph_deploy.hosts.common import mon_add as add # noqa +from ceph_deploy.hosts.common import mon_create as create # noqa diff --git a/ceph_deploy/hosts/arch/uninstall.py b/ceph_deploy/hosts/arch/uninstall.py new file mode 100644 index 0000000..0787392 --- /dev/null +++ b/ceph_deploy/hosts/arch/uninstall.py @@ -0,0 +1,50 @@ +import logging + +from ceph_deploy.util.system import disable_service, stop_service +from ceph_deploy.lib import remoto + + +SYSTEMD_UNITS = [ + 'ceph-mds.target', + 'ceph-mon.target', + 'ceph-osd.target', + 'ceph-radosgw.target', + 'ceph-fuse.target', + 'ceph-mgr.target', + 'ceph-rbd-mirror.target', + 'ceph.target', +] + + +def uninstall(distro, purge=False): + packages = [ + 'ceph', + ] + + hostname = distro.conn.hostname + LOG = logging.getLogger(hostname) + + # I need to stop and disable services prior package removal + LOG.info('stopping and disabling services on {}'.format(hostname)) + for unit in SYSTEMD_UNITS: + stop_service(distro.conn, unit) + disable_service(distro.conn, unit) + + # remoto.process.run( + # distro.conn, + # [ + # 'systemctl', + # 'daemon-reload', + # ] + # ) + + LOG.info('uninstalling packages on {}'.format(hostname)) + distro.packager.remove(packages) + + remoto.process.run( + distro.conn, + [ + 'systemctl', + 'reset-failed', + ] + ) diff --git a/ceph_deploy/hosts/centos/__init__.py b/ceph_deploy/hosts/centos/__init__.py new file mode 100644 index 0000000..daeae4c --- /dev/null +++ b/ceph_deploy/hosts/centos/__init__.py @@ -0,0 +1,34 @@ +from . import mon # noqa +from .install import install, mirror_install, repo_install, repository_url_part, rpm_dist # noqa +from .uninstall import uninstall # noqa +from ceph_deploy.util import pkg_managers +from ceph_deploy.util.system import is_systemd + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + + +def choose_init(module): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + + if module.normalized_release.int_major < 7: + return 'sysvinit' + + if is_systemd(module.conn): + return 'systemd' + + if not module.conn.remote_module.path_exists("/usr/lib/systemd/system/ceph.target"): + return 'sysvinit' + + return 'systemd' + +def get_packager(module): + return pkg_managers.Yum(module) diff --git a/ceph_deploy/hosts/centos/install.py b/ceph_deploy/hosts/centos/install.py new file mode 100644 index 0000000..d622635 --- /dev/null +++ b/ceph_deploy/hosts/centos/install.py @@ -0,0 +1,219 @@ +import logging +from ceph_deploy.util import templates +from ceph_deploy.lib import remoto +from ceph_deploy.hosts.common import map_components +from ceph_deploy.util.paths import gpg +from ceph_deploy.util import net + + +LOG = logging.getLogger(__name__) +NON_SPLIT_PACKAGES = ['ceph-osd', 'ceph-mon', 'ceph-mds'] + + +def rpm_dist(distro): + if distro.normalized_name in ['redhat', 'centos', 'scientific', 'oracle', 'virtuozzo'] and distro.normalized_release.int_major >= 6: + return 'el' + distro.normalized_release.major + return 'el6' + + +def repository_url_part(distro): + """ + Historically everything CentOS, RHEL, and Scientific has been mapped to + `el6` urls, but as we are adding repositories for `rhel`, the URLs should + map correctly to, say, `rhel6` or `rhel7`. + + This function looks into the `distro` object and determines the right url + part for the given distro, falling back to `el6` when all else fails. + + Specifically to work around the issue of CentOS vs RHEL:: + + >>> import platform + >>> platform.linux_distribution() + ('Red Hat Enterprise Linux Server', '7.0', 'Maipo') + + """ + if distro.normalized_release.int_major >= 6: + if distro.normalized_name == 'redhat': + return 'rhel' + distro.normalized_release.major + if distro.normalized_name in ['centos', 'scientific', 'oracle', 'virtuozzo']: + return 'el' + distro.normalized_release.major + + return 'el6' + + +def install(distro, version_kind, version, adjust_repos, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.pop('components', []) + ) + + gpgcheck = kw.pop('gpgcheck', 1) + logger = distro.conn.logger + machine = distro.machine_type + repo_part = repository_url_part(distro) + dist = rpm_dist(distro) + + distro.packager.clean() + + # Get EPEL installed before we continue: + if adjust_repos: + distro.packager.install('epel-release') + distro.packager.install('yum-plugin-priorities') + distro.conn.remote_module.enable_yum_priority_obsoletes() + logger.warning('check_obsoletes has been enabled for Yum priorities plugin') + if version_kind in ['stable', 'testing']: + key = 'release' + else: + key = 'autobuild' + + if adjust_repos: + if version_kind in ['stable', 'testing']: + distro.packager.add_repo_gpg_key(gpg.url(key)) + + if version_kind == 'stable': + url = 'https://download.ceph.com/rpm-{version}/{repo}/'.format( + version=version, + repo=repo_part, + ) + elif version_kind == 'testing': + url = 'https://download.ceph.com/rpm-testing/{repo}/'.format(repo=repo_part) + + # remove any old ceph-release package from prevoius release + remoto.process.run( + distro.conn, + [ + 'yum', + 'remove', + '-y', + 'ceph-release' + ], + ) + remoto.process.run( + distro.conn, + [ + 'yum', + 'install', + '-y', + '{url}noarch/ceph-release-1-0.{dist}.noarch.rpm'.format(url=url, dist=dist), + ], + ) + + elif version_kind in ['dev', 'dev_commit']: + logger.info('skipping install of ceph-release package') + logger.info('repo file will be created manually') + shaman_url = 'https://shaman.ceph.com/api/repos/ceph/{version}/{sha1}/{distro}/{distro_version}/repo/?arch={arch}'.format( + distro=distro.normalized_name, + distro_version=distro.normalized_release.major, + version=kw['args'].dev, + sha1=kw['args'].dev_commit or 'latest', + arch=machine + ) + LOG.debug('fetching repo information from: %s' % shaman_url) + content = net.get_chacra_repo(shaman_url) + mirror_install( + distro, + '', # empty repo_url + None, # no need to use gpg here, repos are unsigned + adjust_repos=True, + extra_installs=False, + gpgcheck=gpgcheck, + repo_content=content + ) + + else: + raise Exception('unrecognized version_kind %s' % version_kind) + + # set the right priority + logger.warning('ensuring that /etc/yum.repos.d/ceph.repo contains a high priority') + distro.conn.remote_module.set_repo_priority(['Ceph', 'Ceph-noarch', 'ceph-source']) + logger.warning('altered ceph.repo priorities to contain: priority=1') + + if packages: + distro.packager.install(packages) + + +def mirror_install(distro, repo_url, gpg_url, adjust_repos, extra_installs=True, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.pop('components', []) + ) + repo_url = repo_url.strip('/') # Remove trailing slashes + gpgcheck = kw.pop('gpgcheck', 1) + + distro.packager.clean() + + if adjust_repos: + if gpg_url: + distro.packager.add_repo_gpg_key(gpg_url) + + ceph_repo_content = templates.ceph_repo.format( + repo_url=repo_url, + gpg_url=gpg_url, + gpgcheck=gpgcheck, + ) + content = kw.get('repo_content', ceph_repo_content) + + distro.conn.remote_module.write_yum_repo(content) + # set the right priority + if distro.packager.name == 'yum': + distro.packager.install('yum-plugin-priorities') + distro.conn.remote_module.set_repo_priority(['Ceph', 'Ceph-noarch', 'ceph-source']) + distro.conn.logger.warning('altered ceph.repo priorities to contain: priority=1') + + + if extra_installs and packages: + distro.packager.install(packages) + + +def repo_install(distro, reponame, baseurl, gpgkey, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.pop('components', []) + ) + logger = distro.conn.logger + # Get some defaults + name = kw.pop('name', '%s repo' % reponame) + enabled = kw.pop('enabled', 1) + gpgcheck = kw.pop('gpgcheck', 1) + install_ceph = kw.pop('install_ceph', False) + proxy = kw.pop('proxy', '') # will get ignored if empty + _type = 'repo-md' + baseurl = baseurl.strip('/') # Remove trailing slashes + + distro.packager.clean() + + if gpgkey: + distro.packager.add_repo_gpg_key(gpgkey) + + repo_content = templates.custom_repo( + reponame=reponame, + name=name, + baseurl=baseurl, + enabled=enabled, + gpgcheck=gpgcheck, + _type=_type, + gpgkey=gpgkey, + proxy=proxy, + **kw + ) + + distro.conn.remote_module.write_yum_repo( + repo_content, + "%s.repo" % reponame + ) + + repo_path = '/etc/yum.repos.d/{reponame}.repo'.format(reponame=reponame) + + # set the right priority + if kw.get('priority'): + if distro.packager.name == 'yum': + distro.packager.install('yum-plugin-priorities') + + distro.conn.remote_module.set_repo_priority([reponame], repo_path) + logger.warning('altered {reponame}.repo priorities to contain: priority=1'.format( + reponame=reponame) + ) + + # Some custom repos do not need to install ceph + if install_ceph and packages: + distro.packager.install(packages) diff --git a/ceph_deploy/hosts/centos/mon/__init__.py b/ceph_deploy/hosts/centos/mon/__init__.py new file mode 100644 index 0000000..f266fb0 --- /dev/null +++ b/ceph_deploy/hosts/centos/mon/__init__.py @@ -0,0 +1,2 @@ +from ceph_deploy.hosts.common import mon_add as add # noqa +from ceph_deploy.hosts.common import mon_create as create # noqa diff --git a/ceph_deploy/hosts/centos/uninstall.py b/ceph_deploy/hosts/centos/uninstall.py new file mode 100644 index 0000000..758c34c --- /dev/null +++ b/ceph_deploy/hosts/centos/uninstall.py @@ -0,0 +1,10 @@ +def uninstall(distro, purge=False): + packages = [ + 'ceph', + 'ceph-release', + 'ceph-common', + 'ceph-radosgw', + ] + + distro.packager.remove(packages) + distro.packager.clean() diff --git a/ceph_deploy/hosts/clear/__init__.py b/ceph_deploy/hosts/clear/__init__.py new file mode 100644 index 0000000..e1b3322 --- /dev/null +++ b/ceph_deploy/hosts/clear/__init__.py @@ -0,0 +1,25 @@ +from . import mon # noqa +from .install import install # noqa +from .uninstall import uninstall # noqa +from ceph_deploy.util import pkg_managers + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + + +def choose_init(module): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + # Currently clearlinux only has systemd. + return 'systemd' + + +def get_packager(module): + return pkg_managers.Swupd(module) diff --git a/ceph_deploy/hosts/clear/install.py b/ceph_deploy/hosts/clear/install.py new file mode 100644 index 0000000..3741c3d --- /dev/null +++ b/ceph_deploy/hosts/clear/install.py @@ -0,0 +1,14 @@ + +def install(distro, version_kind, version, adjust_repos, **kw): + """ + Install bundle that contains ceph on the clear host. + + Since clear does not have alternate channels, we will just run the command + """ + logger = distro.conn.logger + packages = ['storage-cluster'] + + logger.info("Installing storage-cluster bundle") + distro.packager.install( + packages + ) diff --git a/ceph_deploy/hosts/clear/mon/__init__.py b/ceph_deploy/hosts/clear/mon/__init__.py new file mode 100644 index 0000000..f266fb0 --- /dev/null +++ b/ceph_deploy/hosts/clear/mon/__init__.py @@ -0,0 +1,2 @@ +from ceph_deploy.hosts.common import mon_add as add # noqa +from ceph_deploy.hosts.common import mon_create as create # noqa diff --git a/ceph_deploy/hosts/clear/uninstall.py b/ceph_deploy/hosts/clear/uninstall.py new file mode 100644 index 0000000..18423ee --- /dev/null +++ b/ceph_deploy/hosts/clear/uninstall.py @@ -0,0 +1,42 @@ +import logging + +from ceph_deploy.util.system import disable_service, stop_service + +SYSTEMD_UNITS = [ + 'ceph-mds.target', + 'ceph-mon.target', + 'ceph-osd.target', + 'ceph-radosgw.target', + 'ceph-fuse.target', + 'ceph-mgr.target', + 'ceph-rbd-mirror.target', + 'ceph.target', + 'ceph-mds*service', + 'ceph-mon*service', + 'ceph-osd*service', + 'ceph-radosgw*service', + 'ceph-fuse*service', + 'ceph-mgr*service', + 'ceph-rbd-mirror*service', + 'ceph*service', +] + + +def uninstall(distro, purge=False): + + hostname = distro.conn.hostname + LOG = logging.getLogger(hostname) + + # I need to stop and disable services prior package removal + LOG.info('stopping and disabling services on {}'.format(hostname)) + for unit in SYSTEMD_UNITS: + stop_service(distro.conn, unit) + disable_service(distro.conn, unit) + + packages = [ + 'storage-cluster', + 'ceph' + ] + + distro.packager.remove(packages) + distro.packager.clean() diff --git a/ceph_deploy/hosts/common.py b/ceph_deploy/hosts/common.py new file mode 100644 index 0000000..70345dc --- /dev/null +++ b/ceph_deploy/hosts/common.py @@ -0,0 +1,248 @@ +from ceph_deploy.util import paths +from ceph_deploy import conf +from ceph_deploy.lib import remoto +from ceph_deploy.util import constants +from ceph_deploy.util import system + + +def ceph_version(conn): + """ + Log the remote ceph-version by calling `ceph --version` + """ + return remoto.process.run(conn, ['ceph', '--version']) + + +def mon_create(distro, args, monitor_keyring): + hostname = distro.conn.remote_module.shortname() + logger = distro.conn.logger + logger.debug('remote hostname: %s' % hostname) + path = paths.mon.path(args.cluster, hostname) + uid = distro.conn.remote_module.path_getuid(constants.base_path) + gid = distro.conn.remote_module.path_getgid(constants.base_path) + done_path = paths.mon.done(args.cluster, hostname) + init_path = paths.mon.init(args.cluster, hostname, distro.init) + + conf_data = conf.ceph.load_raw(args) + + # write the configuration file + distro.conn.remote_module.write_conf( + args.cluster, + conf_data, + args.overwrite_conf, + ) + + # if the mon path does not exist, create it + distro.conn.remote_module.create_mon_path(path, uid, gid) + + logger.debug('checking for done path: %s' % done_path) + if not distro.conn.remote_module.path_exists(done_path): + logger.debug('done path does not exist: %s' % done_path) + if not distro.conn.remote_module.path_exists(paths.mon.constants.tmp_path): + logger.info('creating tmp path: %s' % paths.mon.constants.tmp_path) + distro.conn.remote_module.makedir(paths.mon.constants.tmp_path) + keyring = paths.mon.keyring(args.cluster, hostname) + + logger.info('creating keyring file: %s' % keyring) + distro.conn.remote_module.write_monitor_keyring( + keyring, + monitor_keyring, + uid, gid, + ) + + user_args = [] + if uid != 0: + user_args = user_args + [ '--setuser', str(uid) ] + if gid != 0: + user_args = user_args + [ '--setgroup', str(gid) ] + + remoto.process.run( + distro.conn, + [ + 'ceph-mon', + '--cluster', args.cluster, + '--mkfs', + '-i', hostname, + '--keyring', keyring, + ] + user_args + ) + + logger.info('unlinking keyring file %s' % keyring) + distro.conn.remote_module.unlink(keyring) + + # create the done file + distro.conn.remote_module.create_done_path(done_path, uid, gid) + + # create init path + distro.conn.remote_module.create_init_path(init_path, uid, gid) + + # start mon service + start_mon_service(distro, args.cluster, hostname) + + +def mon_add(distro, args, monitor_keyring): + hostname = distro.conn.remote_module.shortname() + logger = distro.conn.logger + path = paths.mon.path(args.cluster, hostname) + uid = distro.conn.remote_module.path_getuid(constants.base_path) + gid = distro.conn.remote_module.path_getgid(constants.base_path) + monmap_path = paths.mon.monmap(args.cluster, hostname) + done_path = paths.mon.done(args.cluster, hostname) + init_path = paths.mon.init(args.cluster, hostname, distro.init) + + conf_data = conf.ceph.load_raw(args) + + # write the configuration file + distro.conn.remote_module.write_conf( + args.cluster, + conf_data, + args.overwrite_conf, + ) + + # if the mon path does not exist, create it + distro.conn.remote_module.create_mon_path(path, uid, gid) + + logger.debug('checking for done path: %s' % done_path) + if not distro.conn.remote_module.path_exists(done_path): + logger.debug('done path does not exist: %s' % done_path) + if not distro.conn.remote_module.path_exists(paths.mon.constants.tmp_path): + logger.info('creating tmp path: %s' % paths.mon.constants.tmp_path) + distro.conn.remote_module.makedir(paths.mon.constants.tmp_path) + keyring = paths.mon.keyring(args.cluster, hostname) + + logger.info('creating keyring file: %s' % keyring) + distro.conn.remote_module.write_monitor_keyring( + keyring, + monitor_keyring, + uid, gid, + ) + + # get the monmap + remoto.process.run( + distro.conn, + [ + 'ceph', + '--cluster', args.cluster, + 'mon', + 'getmap', + '-o', + monmap_path, + ], + ) + + # now use it to prepare the monitor's data dir + user_args = [] + if uid != 0: + user_args = user_args + [ '--setuser', str(uid) ] + if gid != 0: + user_args = user_args + [ '--setgroup', str(gid) ] + + remoto.process.run( + distro.conn, + [ + 'ceph-mon', + '--cluster', args.cluster, + '--mkfs', + '-i', hostname, + '--monmap', + monmap_path, + '--keyring', keyring, + ] + user_args + ) + + logger.info('unlinking keyring file %s' % keyring) + distro.conn.remote_module.unlink(keyring) + + # create the done file + distro.conn.remote_module.create_done_path(done_path, uid, gid) + + # create init path + distro.conn.remote_module.create_init_path(init_path, uid, gid) + + # start mon service + start_mon_service(distro, args.cluster, hostname) + + +def map_components(notsplit_packages, components): + """ + Returns a list of packages to install based on component names + + This is done by checking if a component is in notsplit_packages, + if it is, we know we need to install 'ceph' instead of the + raw component name. Essentially, this component hasn't been + 'split' from the master 'ceph' package yet. + """ + packages = set() + + for c in components: + if c in notsplit_packages: + packages.add('ceph') + else: + packages.add(c) + + return list(packages) + + +def start_mon_service(distro, cluster, hostname): + """ + start mon service depending on distro init + """ + if distro.init == 'sysvinit': + service = distro.conn.remote_module.which_service() + remoto.process.run( + distro.conn, + [ + service, + 'ceph', + '-c', + '/etc/ceph/{cluster}.conf'.format(cluster=cluster), + 'start', + 'mon.{hostname}'.format(hostname=hostname) + ], + timeout=7, + ) + system.enable_service(distro.conn) + + elif distro.init == 'upstart': + remoto.process.run( + distro.conn, + [ + 'initctl', + 'emit', + 'ceph-mon', + 'cluster={cluster}'.format(cluster=cluster), + 'id={hostname}'.format(hostname=hostname), + ], + timeout=7, + ) + + elif distro.init == 'systemd': + # enable ceph target for this host (in case it isn't already enabled) + remoto.process.run( + distro.conn, + [ + 'systemctl', + 'enable', + 'ceph.target' + ], + timeout=7, + ) + + # enable and start this mon instance + remoto.process.run( + distro.conn, + [ + 'systemctl', + 'enable', + 'ceph-mon@{hostname}'.format(hostname=hostname), + ], + timeout=7, + ) + remoto.process.run( + distro.conn, + [ + 'systemctl', + 'start', + 'ceph-mon@{hostname}'.format(hostname=hostname), + ], + timeout=7, + ) diff --git a/ceph_deploy/hosts/debian/__init__.py b/ceph_deploy/hosts/debian/__init__.py new file mode 100644 index 0000000..e3e747b --- /dev/null +++ b/ceph_deploy/hosts/debian/__init__.py @@ -0,0 +1,35 @@ +from . import mon # noqa +from .install import install, mirror_install, repo_install # noqa +from .uninstall import uninstall # noqa +from ceph_deploy.util import pkg_managers +from ceph_deploy.util.system import is_systemd, is_upstart + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + + +def choose_init(module): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + # Upstart checks first because when installing ceph, the + # `/lib/systemd/system/ceph.target` file may be created, fooling this + # detection mechanism. + if is_upstart(module.conn): + return 'upstart' + + if is_systemd(module.conn) or module.conn.remote_module.path_exists( + "/lib/systemd/system/ceph.target"): + return 'systemd' + + return 'sysvinit' + + +def get_packager(module): + return pkg_managers.Apt(module) diff --git a/ceph_deploy/hosts/debian/install.py b/ceph_deploy/hosts/debian/install.py new file mode 100644 index 0000000..1aa4431 --- /dev/null +++ b/ceph_deploy/hosts/debian/install.py @@ -0,0 +1,125 @@ +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse +import logging +from ceph_deploy.util.paths import gpg +from ceph_deploy.util import net + + +LOG = logging.getLogger(__name__) + + +def install(distro, version_kind, version, adjust_repos, **kw): + packages = kw.pop('components', []) + codename = distro.codename + machine = distro.machine_type + extra_install_flags = [] + + if version_kind in ['stable', 'testing']: + key = 'release' + else: + key = 'autobuild' + + distro.packager.clean() + distro.packager.install(['ca-certificates', 'apt-transport-https']) + + if adjust_repos: + # Wheezy does not like the download.ceph.com SSL cert + protocol = 'https' + if codename == 'wheezy': + protocol = 'http' + + if version_kind in ['dev', 'dev_commit']: + shaman_url = 'https://shaman.ceph.com/api/repos/ceph/{version}/{sha1}/{distro}/{distro_version}/repo/?arch={arch}'.format( + distro=distro.normalized_name, + distro_version=distro.codename, + version=kw['args'].dev, + sha1=kw['args'].dev_commit or 'latest', + arch=machine + ) + LOG.debug('fetching repo information from: %s' % shaman_url) + chacra_url = net.get_request(shaman_url).geturl() + content = net.get_chacra_repo(shaman_url) + # set the repo priority for the right domain + fqdn = urlparse(chacra_url).hostname + distro.conn.remote_module.set_apt_priority(fqdn) + distro.conn.remote_module.write_sources_list_content(content) + extra_install_flags = ['-o', 'Dpkg::Options::=--force-confnew', '--allow-unauthenticated'] + else: + distro.packager.add_repo_gpg_key(gpg.url(key, protocol=protocol)) + if version_kind == 'stable': + url = '{protocol}://download.ceph.com/debian-{version}/'.format( + protocol=protocol, + version=version, + ) + elif version_kind == 'testing': + url = '{protocol}://download.ceph.com/debian-testing/'.format( + protocol=protocol, + ) + else: + raise RuntimeError('Unknown version kind: %r' % version_kind) + + # set the repo priority for the right domain + fqdn = urlparse(url).hostname + distro.conn.remote_module.set_apt_priority(fqdn) + distro.conn.remote_module.write_sources_list(url, codename) + extra_install_flags = ['-o', 'Dpkg::Options::=--force-confnew'] + + distro.packager.clean() + + # TODO this does not downgrade -- should it? + if packages: + distro.packager.install( + packages, + extra_install_flags=extra_install_flags + ) + + +def mirror_install(distro, repo_url, gpg_url, adjust_repos, **kw): + packages = kw.pop('components', []) + version_kind = kw['args'].version_kind + repo_url = repo_url.strip('/') # Remove trailing slashes + + if adjust_repos: + distro.packager.add_repo_gpg_key(gpg_url) + + # set the repo priority for the right domain + fqdn = urlparse(repo_url).hostname + distro.conn.remote_module.set_apt_priority(fqdn) + + distro.conn.remote_module.write_sources_list(repo_url, distro.codename) + + extra_install_flags = ['--allow-unauthenticated'] if version_kind in 'dev' else [] + + if packages: + distro.packager.clean() + distro.packager.install( + packages, + extra_install_flags=extra_install_flags) + + +def repo_install(distro, repo_name, baseurl, gpgkey, **kw): + packages = kw.pop('components', []) + # Get some defaults + safe_filename = '%s.list' % repo_name.replace(' ', '-') + install_ceph = kw.pop('install_ceph', False) + baseurl = baseurl.strip('/') # Remove trailing slashes + + distro.packager.add_repo_gpg_key(gpgkey) + + distro.conn.remote_module.write_sources_list( + baseurl, + distro.codename, + safe_filename + ) + + # set the repo priority for the right domain + fqdn = urlparse(baseurl).hostname + distro.conn.remote_module.set_apt_priority(fqdn) + + # repo is not operable until an update + distro.packager.clean() + + if install_ceph and packages: + distro.packager.install(packages) diff --git a/ceph_deploy/hosts/debian/mon/__init__.py b/ceph_deploy/hosts/debian/mon/__init__.py new file mode 100644 index 0000000..f266fb0 --- /dev/null +++ b/ceph_deploy/hosts/debian/mon/__init__.py @@ -0,0 +1,2 @@ +from ceph_deploy.hosts.common import mon_add as add # noqa +from ceph_deploy.hosts.common import mon_create as create # noqa diff --git a/ceph_deploy/hosts/debian/uninstall.py b/ceph_deploy/hosts/debian/uninstall.py new file mode 100644 index 0000000..b3a01b2 --- /dev/null +++ b/ceph_deploy/hosts/debian/uninstall.py @@ -0,0 +1,15 @@ +def uninstall(distro, purge=False): + packages = [ + 'ceph', + 'ceph-mds', + 'ceph-common', + 'ceph-fs-common', + 'radosgw', + ] + extra_remove_flags = [] + if purge: + extra_remove_flags.append('--purge') + distro.packager.remove( + packages, + extra_remove_flags=extra_remove_flags + ) diff --git a/ceph_deploy/hosts/fedora/__init__.py b/ceph_deploy/hosts/fedora/__init__.py new file mode 100644 index 0000000..81d8aca --- /dev/null +++ b/ceph_deploy/hosts/fedora/__init__.py @@ -0,0 +1,30 @@ +from . import mon # noqa +from ceph_deploy.hosts.centos.install import repo_install # noqa +from .install import install, mirror_install # noqa +from .uninstall import uninstall # noqa +from ceph_deploy.util import pkg_managers + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + +def choose_init(module): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + + if not module.conn.remote_module.path_exists("/usr/lib/systemd/system/ceph.target"): + return 'sysvinit' + + return 'systemd' + +def get_packager(module): + if module.normalized_release.int_major >= 22: + return pkg_managers.DNF(module) + else: + return pkg_managers.Yum(module) diff --git a/ceph_deploy/hosts/fedora/install.py b/ceph_deploy/hosts/fedora/install.py new file mode 100644 index 0000000..b2806f4 --- /dev/null +++ b/ceph_deploy/hosts/fedora/install.py @@ -0,0 +1,87 @@ +from ceph_deploy.lib import remoto +from ceph_deploy.hosts.centos.install import repo_install, mirror_install # noqa +from ceph_deploy.util.paths import gpg +from ceph_deploy.hosts.common import map_components + + +NON_SPLIT_PACKAGES = ['ceph-osd', 'ceph-mon', 'ceph-mds'] + + +def install(distro, version_kind, version, adjust_repos, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.pop('components', []) + ) + gpgcheck = kw.pop('gpgcheck', 1) + + logger = distro.conn.logger + release = distro.release + machine = distro.machine_type + + if version_kind in ['stable', 'testing']: + key = 'release' + else: + key = 'autobuild' + + if adjust_repos: + if distro.packager.name == 'yum': + distro.packager.install('yum-plugin-priorities') + # haven't been able to determine necessity of check_obsoletes with DNF + distro.conn.remote_module.enable_yum_priority_obsoletes() + logger.warning('check_obsoletes has been enabled for Yum priorities plugin') + + if version_kind in ['stable', 'testing']: + distro.packager.add_repo_gpg_key(gpg.url(key)) + + if version_kind == 'stable': + url = 'https://download.ceph.com/rpm-{version}/fc{release}/'.format( + version=version, + release=release, + ) + elif version_kind == 'testing': + url = 'https://download.ceph.com/rpm-testing/fc{release}'.format( + release=release, + ) + + remoto.process.run( + distro.conn, + [ + 'rpm', + '-Uvh', + '--replacepkgs', + '--force', + '--quiet', + '{url}noarch/ceph-release-1-0.fc{release}.noarch.rpm'.format( + url=url, + release=release, + ), + ] + ) + + # set the right priority + logger.warning('ensuring that /etc/yum.repos.d/ceph.repo contains a high priority') + distro.conn.remote_module.set_repo_priority(['Ceph', 'Ceph-noarch', 'ceph-source']) + logger.warning('altered ceph.repo priorities to contain: priority=1') + + elif version_kind in ['dev', 'dev_commit']: + logger.info('skipping install of ceph-release package') + logger.info('repo file will be created manually') + mirror_install( + distro, + 'http://gitbuilder.ceph.com/ceph-rpm-fc{release}-{machine}-basic/{sub}/{version}/'.format( + release=release.split(".", 1)[0], + machine=machine, + sub='ref' if version_kind == 'dev' else 'sha1', + version=version), + gpg.url(key), + adjust_repos=True, + extra_installs=False, + gpgcheck=gpgcheck, + ) + + else: + raise Exception('unrecognized version_kind %s' % version_kind) + + distro.packager.install( + packages + ) diff --git a/ceph_deploy/hosts/fedora/mon/__init__.py b/ceph_deploy/hosts/fedora/mon/__init__.py new file mode 100644 index 0000000..f266fb0 --- /dev/null +++ b/ceph_deploy/hosts/fedora/mon/__init__.py @@ -0,0 +1,2 @@ +from ceph_deploy.hosts.common import mon_add as add # noqa +from ceph_deploy.hosts.common import mon_create as create # noqa diff --git a/ceph_deploy/hosts/fedora/uninstall.py b/ceph_deploy/hosts/fedora/uninstall.py new file mode 100644 index 0000000..8d40909 --- /dev/null +++ b/ceph_deploy/hosts/fedora/uninstall.py @@ -0,0 +1,8 @@ +def uninstall(distro, purge=False): + packages = [ + 'ceph', + 'ceph-common', + 'ceph-radosgw', + ] + + distro.packager.remove(packages) diff --git a/ceph_deploy/hosts/remotes.py b/ceph_deploy/hosts/remotes.py new file mode 100644 index 0000000..049ebff --- /dev/null +++ b/ceph_deploy/hosts/remotes.py @@ -0,0 +1,417 @@ +try: + import configparser +except ImportError: + import ConfigParser as configparser +import errno +import socket +import os +import shutil +import tempfile +import platform +import re + + +def platform_information(_linux_distribution=None): + """ detect platform information from remote host """ + distro = release = codename = None + try: + linux_distribution = _linux_distribution or platform.linux_distribution + distro, release, codename = linux_distribution() + except AttributeError: + # NOTE: py38 does not have platform.linux_distribution + pass + if not distro: + distro, release, codename = parse_os_release() + if not codename and 'debian' in distro.lower(): # this could be an empty string in Debian + debian_codenames = { + '10': 'buster', + '9': 'stretch', + '8': 'jessie', + '7': 'wheezy', + '6': 'squeeze', + } + major_version = release.split('.')[0] + codename = debian_codenames.get(major_version, '') + + # In order to support newer jessie/sid or wheezy/sid strings we test this + # if sid is buried in the minor, we should use sid anyway. + if not codename and '/' in release: + major, minor = release.split('/') + if minor == 'sid': + codename = minor + else: + codename = major + if not codename and 'oracle' in distro.lower(): # this could be an empty string in Oracle linux + codename = 'oracle' + if not codename and 'virtuozzo linux' in distro.lower(): # this could be an empty string in Virtuozzo linux + codename = 'virtuozzo' + if not codename and 'arch' in distro.lower(): # this could be an empty string in Arch linux + codename = 'arch' + + return ( + str(distro).rstrip(), + str(release).rstrip(), + str(codename).rstrip() + ) + + +def parse_os_release(release_path='/etc/os-release'): + """ Extract (distro, release, codename) from /etc/os-release if present """ + release_info = {} + if os.path.isfile(release_path): + for line in open(release_path, 'r').readlines(): + line = line.strip() + if line.startswith('#'): + continue + parts = line.split('=') + if len(parts) != 2: + continue + release_info[parts[0].strip()] = parts[1].strip("\"'\n\t ") + # In theory, we want ID/NAME, VERSION_ID and VERSION_CODENAME (with a + # possible fallback to VERSION on the latter), based on information at: + # https://www.freedesktop.org/software/systemd/man/os-release.html + # However, after reviewing several distros /etc/os-release, getting + # the codename is a bit of a mess. It's usually in parentheses in + # VERSION, with some exceptions. + distro = release_info.get('ID', '') + release = release_info.get('VERSION_ID', '') + codename = release_info.get('UBUNTU_CODENAME', release_info.get('VERSION', '')) + match = re.match(r'^[^(]+ \(([^)]+)\)', codename) + if match: + codename = match.group(1).lower() + if not codename and release_info.get('NAME', '') == 'openSUSE Tumbleweed': + codename = 'tumbleweed' + return (distro, release, codename) + +def machine_type(): + """ detect machine type """ + return platform.machine() + + +def write_sources_list(url, codename, filename='ceph.list', mode=0o644): + """add deb repo to /etc/apt/sources.list.d/""" + repo_path = os.path.join('/etc/apt/sources.list.d', filename) + content = 'deb {url} {codename} main\n'.format( + url=url, + codename=codename, + ) + write_file(repo_path, content.encode('utf-8'), mode) + + +def write_sources_list_content(content, filename='ceph.list', mode=0o644): + """add deb repo to /etc/apt/sources.list.d/ from content""" + repo_path = os.path.join('/etc/apt/sources.list.d', filename) + if not isinstance(content, str): + content = content.decode('utf-8') + write_file(repo_path, content.encode('utf-8'), mode) + + +def write_yum_repo(content, filename='ceph.repo'): + """add yum repo file in /etc/yum.repos.d/""" + repo_path = os.path.join('/etc/yum.repos.d', filename) + if not isinstance(content, str): + content = content.decode('utf-8') + write_file(repo_path, content.encode('utf-8')) + + +def set_apt_priority(fqdn, path='/etc/apt/preferences.d/ceph.pref'): + template = "Package: *\nPin: origin {fqdn}\nPin-Priority: 999\n" + content = template.format(fqdn=fqdn) + with open(path, 'w') as fout: + fout.write(content) + + +def set_repo_priority(sections, path='/etc/yum.repos.d/ceph.repo', priority='1'): + Config = configparser.ConfigParser() + Config.read(path) + Config.sections() + for section in sections: + try: + Config.set(section, 'priority', priority) + except configparser.NoSectionError: + # Emperor versions of Ceph used all lowercase sections + # so lets just try again for the section that failed, maybe + # we are able to find it if it is lower + Config.set(section.lower(), 'priority', priority) + + with open(path, 'w') as fout: + Config.write(fout) + + # And now, because ConfigParser is super duper, we need to remove the + # assignments so this looks like it was before + def remove_whitespace_from_assignments(): + separator = "=" + lines = open(path).readlines() + fp = open(path, "w") + for line in lines: + line = line.strip() + if not line.startswith("#") and separator in line: + assignment = line.split(separator, 1) + assignment = tuple(map(str.strip, assignment)) + fp.write("%s%s%s\n" % (assignment[0], separator, assignment[1])) + else: + fp.write(line + "\n") + + remove_whitespace_from_assignments() + + +def write_conf(cluster, conf, overwrite): + """ write cluster configuration to /etc/ceph/{cluster}.conf """ + path = '/etc/ceph/{cluster}.conf'.format(cluster=cluster) + tmp_file = tempfile.NamedTemporaryFile('w', dir='/etc/ceph', delete=False) + err_msg = 'config file %s exists with different content; use --overwrite-conf to overwrite' % path + + if not os.path.isdir('/etc/ceph'): + err_msg = '/etc/ceph/ does not exist - could not write config' + raise RuntimeError(err_msg) + + if os.path.exists(path): + with open(path, 'r') as f: + old = f.read() + if old != conf and not overwrite: + raise RuntimeError(err_msg) + tmp_file.write(conf) + tmp_file.close() + shutil.move(tmp_file.name, path) + else: + with open(path, 'w') as f: + f.write(conf) + os.chmod(path, 0o644) + + +def write_keyring(path, key, uid=-1, gid=-1): + """ create a keyring file """ + # Note that we *require* to avoid deletion of the temp file + # otherwise we risk not being able to copy the contents from + # one file system to the other, hence the `delete=False` + tmp_file = tempfile.NamedTemporaryFile('wb', delete=False) + tmp_file.write(key) + tmp_file.close() + keyring_dir = os.path.dirname(path) + if not path_exists(keyring_dir): + makedir(keyring_dir, uid, gid) + shutil.move(tmp_file.name, path) + + +def create_mon_path(path, uid=-1, gid=-1): + """create the mon path if it does not exist""" + if not os.path.exists(path): + os.makedirs(path) + os.chown(path, uid, gid); + + +def create_done_path(done_path, uid=-1, gid=-1): + """create a done file to avoid re-doing the mon deployment""" + with open(done_path, 'wb'): + pass + os.chown(done_path, uid, gid); + + +def create_init_path(init_path, uid=-1, gid=-1): + """create the init path if it does not exist""" + if not os.path.exists(init_path): + with open(init_path, 'wb'): + pass + os.chown(init_path, uid, gid); + + +def append_to_file(file_path, contents): + """append contents to file""" + with open(file_path, 'a') as f: + f.write(contents) + +def path_getuid(path): + return os.stat(path).st_uid + +def path_getgid(path): + return os.stat(path).st_gid + +def readline(path): + with open(path) as _file: + return _file.readline().strip('\n') + + +def path_exists(path): + return os.path.exists(path) + + +def get_realpath(path): + return os.path.realpath(path) + + +def listdir(path): + return os.listdir(path) + + +def makedir(path, ignored=None, uid=-1, gid=-1): + ignored = ignored or [] + try: + os.makedirs(path) + except OSError as error: + if error.errno in ignored: + pass + else: + # re-raise the original exception + raise + else: + os.chown(path, uid, gid); + + +def unlink(_file): + os.unlink(_file) + + +def write_monitor_keyring(keyring, monitor_keyring, uid=-1, gid=-1): + """create the monitor keyring file""" + write_file(keyring, monitor_keyring, 0o600, None, uid, gid) + + +def write_file(path, content, mode=0o644, directory=None, uid=-1, gid=-1): + if directory: + if path.startswith("/"): + path = path[1:] + path = os.path.join(directory, path) + if os.path.exists(path): + # Delete file in case we are changing its mode + os.unlink(path) + with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT, mode), 'wb') as f: + f.write(content) + os.chown(path, uid, gid) + + +def touch_file(path): + with open(path, 'wb') as f: # noqa + pass + + +def get_file(path): + """ fetch remote file """ + try: + with open(path, 'rb') as f: + return f.read() + except IOError: + pass + + +def object_grep(term, file_object): + for line in file_object.readlines(): + if term in line: + return True + return False + + +def grep(term, file_path): + # A small grep-like function that will search for a word in a file and + # return True if it does and False if it does not. + + # Implemented initially to have a similar behavior as the init system + # detection in Ceph's init scripts:: + + # # detect systemd + # # SYSTEMD=0 + # grep -qs systemd /proc/1/comm && SYSTEMD=1 + + # .. note:: Because we intent to be operating in silent mode, we explicitly + # return ``False`` if the file does not exist. + if not os.path.isfile(file_path): + return False + + with open(file_path) as _file: + return object_grep(term, _file) + + +def shortname(): + """get remote short hostname""" + return socket.gethostname().split('.', 1)[0] + + +def which_service(): + """ locating the `service` executable... """ + # XXX This should get deprecated at some point. For now + # it just bypasses and uses the new helper. + return which('service') + + +def which(executable): + """find the location of an executable""" + locations = ( + '/usr/local/bin', + '/bin', + '/usr/bin', + '/usr/local/sbin', + '/usr/sbin', + '/sbin', + ) + + for location in locations: + executable_path = os.path.join(location, executable) + if os.path.exists(executable_path) and os.path.isfile(executable_path): + return executable_path + + +def make_mon_removed_dir(path, file_name): + """ move old monitor data """ + try: + os.makedirs('/var/lib/ceph/mon-removed') + except OSError as e: + if e.errno != errno.EEXIST: + raise + shutil.move(path, os.path.join('/var/lib/ceph/mon-removed/', file_name)) + + +def safe_mkdir(path, uid=-1, gid=-1): + """ create path if it doesn't exist """ + try: + os.mkdir(path) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + else: + os.chown(path, uid, gid) + + +def safe_makedirs(path, uid=-1, gid=-1): + """ create path recursively if it doesn't exist """ + try: + os.makedirs(path) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + else: + os.chown(path, uid, gid) + + +def zeroing(dev): + """ zeroing last few blocks of device """ + # this kills the crab + # + # sgdisk will wipe out the main copy of the GPT partition + # table (sorry), but it doesn't remove the backup copies, and + # subsequent commands will continue to complain and fail when + # they see those. zeroing the last few blocks of the device + # appears to do the trick. + lba_size = 4096 + size = 33 * lba_size + return True + with open(dev, 'wb') as f: + f.seek(-size, os.SEEK_END) + f.write(size*b'\0') + + +def enable_yum_priority_obsoletes(path="/etc/yum/pluginconf.d/priorities.conf"): + """Configure Yum priorities to include obsoletes""" + config = configparser.ConfigParser() + config.read(path) + config.set('main', 'check_obsoletes', '1') + with open(path, 'w') as fout: + config.write(fout) + + +# remoto magic, needed to execute these functions remotely +if __name__ == '__channelexec__': + for item in channel: # noqa + channel.send(eval(item)) # noqa diff --git a/ceph_deploy/hosts/rhel/__init__.py b/ceph_deploy/hosts/rhel/__init__.py new file mode 100644 index 0000000..a86ad50 --- /dev/null +++ b/ceph_deploy/hosts/rhel/__init__.py @@ -0,0 +1,33 @@ +from . import mon # noqa +from .install import install, mirror_install, repo_install # noqa +from .uninstall import uninstall # noqa +from ceph_deploy.util import pkg_managers +from ceph_deploy.util.system import is_systemd + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + +def choose_init(module): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + + if module.normalized_release.int_major < 7: + return 'sysvinit' + + if not module.conn.remote_module.path_exists("/usr/lib/systemd/system/ceph.target"): + return 'sysvinit' + + if is_systemd(module.conn): + return 'systemd' + + return 'systemd' + +def get_packager(module): + return pkg_managers.Yum(module) diff --git a/ceph_deploy/hosts/rhel/install.py b/ceph_deploy/hosts/rhel/install.py new file mode 100644 index 0000000..bf44a03 --- /dev/null +++ b/ceph_deploy/hosts/rhel/install.py @@ -0,0 +1,71 @@ +from ceph_deploy.util import templates + + +def install(distro, version_kind, version, adjust_repos, **kw): + packages = kw.get('components', []) + distro.packager.clean() + distro.packager.install(packages) + + +def mirror_install(distro, repo_url, + gpg_url, adjust_repos, extra_installs=True, **kw): + packages = kw.get('components', []) + repo_url = repo_url.strip('/') # Remove trailing slashes + gpgcheck = kw.pop('gpgcheck', 1) + + distro.packager.clean() + + if adjust_repos: + distro.packager.add_repo_gpg_key(gpg_url) + + ceph_repo_content = templates.ceph_repo.format( + repo_url=repo_url, + gpg_url=gpg_url, + gpgcheck=gpgcheck, + ) + + distro.conn.remote_module.write_yum_repo(ceph_repo_content) + + if extra_installs and packages: + distro.packager.install(packages) + + +def repo_install(distro, reponame, baseurl, gpgkey, **kw): + # do we have specific components to install? + # removed them from `kw` so that we don't mess with other defaults + packages = kw.pop('components', []) + + # Get some defaults + name = kw.pop('name', '%s repo' % reponame) + enabled = kw.pop('enabled', 1) + gpgcheck = kw.pop('gpgcheck', 1) + install_ceph = kw.pop('install_ceph', False) + proxy = kw.pop('proxy', '') # will get ignored if empty + _type = 'repo-md' + baseurl = baseurl.strip('/') # Remove trailing slashes + + distro.packager.clean() + + if gpgkey: + distro.packager.add_repo_gpg_key(gpgkey) + + repo_content = templates.custom_repo( + reponame=reponame, + name=name, + baseurl=baseurl, + enabled=enabled, + gpgcheck=gpgcheck, + _type=_type, + gpgkey=gpgkey, + proxy=proxy, + **kw + ) + + distro.conn.remote_module.write_yum_repo( + repo_content, + "%s.repo" % reponame + ) + + # Some custom repos do not need to install ceph + if install_ceph and packages: + distro.packager.install(packages) diff --git a/ceph_deploy/hosts/rhel/mon/__init__.py b/ceph_deploy/hosts/rhel/mon/__init__.py new file mode 100644 index 0000000..f266fb0 --- /dev/null +++ b/ceph_deploy/hosts/rhel/mon/__init__.py @@ -0,0 +1,2 @@ +from ceph_deploy.hosts.common import mon_add as add # noqa +from ceph_deploy.hosts.common import mon_create as create # noqa diff --git a/ceph_deploy/hosts/rhel/uninstall.py b/ceph_deploy/hosts/rhel/uninstall.py new file mode 100644 index 0000000..17ae208 --- /dev/null +++ b/ceph_deploy/hosts/rhel/uninstall.py @@ -0,0 +1,11 @@ +def uninstall(distro, purge=False): + packages = [ + 'ceph', + 'ceph-common', + 'ceph-mon', + 'ceph-osd', + 'ceph-radosgw' + ] + + distro.packager.remove(packages) + distro.packager.clean() diff --git a/ceph_deploy/hosts/suse/__init__.py b/ceph_deploy/hosts/suse/__init__.py new file mode 100644 index 0000000..af66a15 --- /dev/null +++ b/ceph_deploy/hosts/suse/__init__.py @@ -0,0 +1,31 @@ +from . import mon # noqa +from .install import install, mirror_install, repo_install # noqa +from .uninstall import uninstall # noqa +import logging + +from ceph_deploy.util import pkg_managers + +# Allow to set some information about this distro +# + +log = logging.getLogger(__name__) + +distro = None +release = None +codename = None + +def choose_init(module): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + init_mapping = { '11' : 'sysvinit', # SLE_11 + '12' : 'systemd', # SLE_12 + '13.1' : 'systemd', # openSUSE_13.1 + } + return init_mapping.get(release, 'systemd') + + +def get_packager(module): + return pkg_managers.Zypper(module) diff --git a/ceph_deploy/hosts/suse/install.py b/ceph_deploy/hosts/suse/install.py new file mode 100644 index 0000000..7a23f38 --- /dev/null +++ b/ceph_deploy/hosts/suse/install.py @@ -0,0 +1,98 @@ +import logging + +from ceph_deploy.util import templates +from ceph_deploy.lib import remoto +from ceph_deploy.hosts.common import map_components + +LOG = logging.getLogger(__name__) + +NON_SPLIT_PACKAGES = ['ceph-osd', 'ceph-mon', 'ceph-mds'] + + +def install(distro, version_kind, version, adjust_repos, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.get('components', []) + ) + + distro.packager.clean() + if packages: + distro.packager.install(packages) + + +def mirror_install(distro, repo_url, gpg_url, adjust_repos, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.get('components', []) + ) + repo_url = repo_url.strip('/') # Remove trailing slashes + gpg_url_path = gpg_url.split('file://')[-1] # Remove file if present + gpgcheck = kw.pop('gpgcheck', 1) + + if adjust_repos: + remoto.process.run( + distro.conn, + [ + 'rpm', + '--import', + gpg_url_path, + ] + ) + + ceph_repo_content = templates.zypper_repo.format( + repo_url=repo_url, + gpg_url=gpg_url, + gpgcheck=gpgcheck, + ) + distro.conn.remote_module.write_file( + '/etc/zypp/repos.d/ceph.repo', + ceph_repo_content.encode('utf-8')) + distro.packager.clean() + + if packages: + distro.packager.install(packages) + + +def repo_install(distro, reponame, baseurl, gpgkey, **kw): + packages = map_components( + NON_SPLIT_PACKAGES, + kw.pop('components', []) + ) + # Get some defaults + name = kw.get('name', '%s repo' % reponame) + enabled = kw.get('enabled', 1) + gpgcheck = kw.get('gpgcheck', 1) + install_ceph = kw.pop('install_ceph', False) + proxy = kw.get('proxy') + _type = 'repo-md' + baseurl = baseurl.strip('/') # Remove trailing slashes + + if gpgkey: + remoto.process.run( + distro.conn, + [ + 'rpm', + '--import', + gpgkey, + ] + ) + + repo_content = templates.custom_repo( + reponame=reponame, + name = name, + baseurl = baseurl, + enabled = enabled, + gpgcheck = gpgcheck, + _type = _type, + gpgkey = gpgkey, + proxy = proxy, + ) + + distro.conn.remote_module.write_file( + '/etc/zypp/repos.d/%s' % (reponame), + repo_content.encode('utf-8') + ) + + # Some custom repos do not need to install ceph + if install_ceph and packages: + distro.packager.install(packages) diff --git a/ceph_deploy/hosts/suse/mon/__init__.py b/ceph_deploy/hosts/suse/mon/__init__.py new file mode 100644 index 0000000..f266fb0 --- /dev/null +++ b/ceph_deploy/hosts/suse/mon/__init__.py @@ -0,0 +1,2 @@ +from ceph_deploy.hosts.common import mon_add as add # noqa +from ceph_deploy.hosts.common import mon_create as create # noqa diff --git a/ceph_deploy/hosts/suse/uninstall.py b/ceph_deploy/hosts/suse/uninstall.py new file mode 100644 index 0000000..163d891 --- /dev/null +++ b/ceph_deploy/hosts/suse/uninstall.py @@ -0,0 +1,10 @@ +def uninstall(distro, purge=False): + packages = [ + 'ceph', + 'ceph-common', + 'libcephfs1', + 'librados2', + 'librbd1', + 'ceph-radosgw', + ] + distro.packager.remove(packages) diff --git a/ceph_deploy/hosts/util.py b/ceph_deploy/hosts/util.py new file mode 100644 index 0000000..b943609 --- /dev/null +++ b/ceph_deploy/hosts/util.py @@ -0,0 +1,31 @@ +""" +A utility module that can host utilities that will be used by more than +one type of distro and not common to all of them +""" +from ceph_deploy.util import pkg_managers + + +def install_yum_priorities(distro, _yum=None): + """ + EPEL started packaging Ceph so we need to make sure that the ceph.repo we + install has a higher priority than the EPEL repo so that when installing + Ceph it will come from the repo file we create. + + The name of the package changed back and forth (!) since CentOS 4: + + From the CentOS wiki:: + + Note: This plugin has carried at least two differing names over time. + It is named yum-priorities on CentOS-5 but was named + yum-plugin-priorities on CentOS-4. CentOS-6 has reverted to + yum-plugin-priorities. + + :params _yum: Used for testing, so we can inject a fake yum + """ + yum = _yum or pkg_managers.yum + package_name = 'yum-plugin-priorities' + + if distro.normalized_name == 'centos': + if distro.release[0] != '6': + package_name = 'yum-priorities' + yum(distro.conn, package_name) diff --git a/ceph_deploy/install.py b/ceph_deploy/install.py new file mode 100644 index 0000000..12c5f79 --- /dev/null +++ b/ceph_deploy/install.py @@ -0,0 +1,670 @@ +import argparse +import logging +import os + +from ceph_deploy import hosts +from ceph_deploy.cliutil import priority +from ceph_deploy.lib import remoto +from ceph_deploy.util.constants import default_components +from ceph_deploy.util.paths import gpg + +LOG = logging.getLogger(__name__) + + +def sanitize_args(args): + """ + args may need a bunch of logic to set proper defaults that argparse is + not well suited for. + """ + if args.release is None: + args.release = 'nautilus' + args.default_release = True + + # XXX This whole dance is because --stable is getting deprecated + if args.stable is not None: + LOG.warning('the --stable flag is deprecated, use --release instead') + args.release = args.stable + # XXX Tango ends here. + + return args + + +def detect_components(args, distro): + """ + Since the package split, now there are various different Ceph components to + install like: + + * ceph + * ceph-mon + * ceph-mgr + * ceph-osd + * ceph-mds + + This helper function should parse the args that may contain specifics about + these flags and return the default if none are passed in (which is, install + everything) + """ + # the flag that prevents all logic here is the `--repo` flag which is used + # when no packages should be installed, just the repo files, so check for + # that here and return an empty list (which is equivalent to say 'no + # packages should be installed') + if args.repo: + return [] + + flags = { + 'install_osd': 'ceph-osd', + 'install_rgw': 'ceph-radosgw', + 'install_mds': 'ceph-mds', + 'install_mon': 'ceph-mon', + 'install_mgr': 'ceph-mgr', + 'install_common': 'ceph-common', + 'install_tests': 'ceph-test', + } + + if distro.is_rpm: + defaults = default_components.rpm + elif distro.is_pkgtarxz: + # archlinux doesn't have components! + flags = { + 'install_osd': 'ceph', + 'install_rgw': 'ceph', + 'install_mds': 'ceph', + 'install_mon': 'ceph', + 'install_mgr': 'ceph', + 'install_common': 'ceph', + 'install_tests': 'ceph', + } + defaults = default_components.pkgtarxz + else: + defaults = default_components.deb + # different naming convention for deb than rpm for radosgw + flags['install_rgw'] = 'radosgw' + + if args.install_all: + return defaults + else: + components = [] + for k, v in flags.items(): + if getattr(args, k, False): + components.append(v) + # if we have some components selected from flags then return that, + # otherwise return defaults because no flags and no `--repo` means we + # should get all of them by default + return components or defaults + + +def install(args): + args = sanitize_args(args) + + if args.repo: + return install_repo(args) + + gpgcheck = 0 if args.nogpgcheck else 1 + + if args.version_kind == 'stable': + version = args.release + else: + version = getattr(args, args.version_kind) + + version_str = args.version_kind + + if version: + version_str += ' version {version}'.format(version=version) + LOG.debug( + 'Installing %s on cluster %s hosts %s', + version_str, + args.cluster, + ' '.join(args.host), + ) + + for hostname in args.host: + LOG.debug('Detecting platform for host %s ...', hostname) + distro = hosts.get( + hostname, + username=args.username, + # XXX this should get removed once ceph packages are split for + # upstream. If default_release is True, it means that the user is + # trying to install on a RHEL machine and should expect to get RHEL + # packages. Otherwise, it will need to specify either a specific + # version, or repo, or a development branch. Other distro users + # should not see any differences. + use_rhceph=args.default_release, + ) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + components = detect_components(args, distro) + if distro.init == 'sysvinit' and args.cluster != 'ceph': + LOG.error('refusing to install on host: %s, with custom cluster name: %s' % ( + hostname, + args.cluster, + ) + ) + LOG.error('custom cluster names are not supported on sysvinit hosts') + continue + + rlogger = logging.getLogger(hostname) + rlogger.info('installing Ceph on %s' % hostname) + + cd_conf = getattr(args, 'cd_conf', None) + + # custom repo arguments + repo_url = os.environ.get('CEPH_DEPLOY_REPO_URL') or args.repo_url + gpg_url = os.environ.get('CEPH_DEPLOY_GPG_URL') or args.gpg_url + gpg_fallback = gpg.url('release') + + if gpg_url is None and repo_url: + LOG.warning('--gpg-url was not used, will fallback') + LOG.warning('using GPG fallback: %s', gpg_fallback) + gpg_url = gpg_fallback + + if args.local_mirror: + if args.username: + hostname = "%s@%s" % (args.username, hostname) + remoto.rsync(hostname, args.local_mirror, '/opt/ceph-deploy/repo', distro.conn.logger, sudo=True) + repo_url = 'file:///opt/ceph-deploy/repo' + gpg_url = 'file:///opt/ceph-deploy/repo/release.asc' + + if repo_url: # triggers using a custom repository + # the user used a custom repo url, this should override anything + # we can detect from the configuration, so warn about it + if cd_conf: + if cd_conf.get_default_repo(): + rlogger.warning('a default repo was found but it was \ + overridden on the CLI') + if args.release in cd_conf.get_repos(): + rlogger.warning('a custom repo was found but it was \ + overridden on the CLI') + + rlogger.info('using custom repository location: %s', repo_url) + distro.mirror_install( + distro, + repo_url, + gpg_url, + args.adjust_repos, + components=components, + gpgcheck=gpgcheck, + args=args + ) + + # Detect and install custom repos here if needed + elif should_use_custom_repo(args, cd_conf, repo_url): + LOG.info('detected valid custom repositories from config file') + custom_repo(distro, args, cd_conf, rlogger) + + else: # otherwise a normal installation + distro.install( + distro, + args.version_kind, + version, + args.adjust_repos, + components=components, + gpgcheck = gpgcheck, + args=args + ) + + # Check the ceph version we just installed + hosts.common.ceph_version(distro.conn) + distro.conn.exit() + + +def should_use_custom_repo(args, cd_conf, repo_url): + """ + A boolean to determine the logic needed to proceed with a custom repo + installation instead of cramming everything nect to the logic operator. + """ + if repo_url: + # repo_url signals a CLI override, return False immediately + return False + if cd_conf: + if cd_conf.has_repos: + has_valid_release = args.release in cd_conf.get_repos() + has_default_repo = cd_conf.get_default_repo() + if has_valid_release or has_default_repo: + return True + return False + + +def custom_repo(distro, args, cd_conf, rlogger, install_ceph=None): + """ + A custom repo install helper that will go through config checks to retrieve + repos (and any extra repos defined) and install those + + ``cd_conf`` is the object built from argparse that holds the flags and + information needed to determine what metadata from the configuration to be + used. + """ + default_repo = cd_conf.get_default_repo() + components = detect_components(args, distro) + if args.release in cd_conf.get_repos(): + LOG.info('will use repository from conf: %s' % args.release) + default_repo = args.release + elif default_repo: + LOG.info('will use default repository: %s' % default_repo) + + # At this point we know there is a cd_conf and that it has custom + # repos make sure we were able to detect and actual repo + if not default_repo: + LOG.warning('a ceph-deploy config was found with repos \ + but could not default to one') + else: + options = dict(cd_conf.items(default_repo)) + options['install_ceph'] = False if install_ceph is False else True + extra_repos = cd_conf.get_list(default_repo, 'extra-repos') + rlogger.info('adding custom repository file') + try: + distro.repo_install( + distro, + default_repo, + options.pop('baseurl'), + options.pop('gpgkey'), + components=components, + **options + ) + except KeyError as err: + raise RuntimeError('missing required key: %s in config section: %s' % (err, default_repo)) + + for xrepo in extra_repos: + rlogger.info('adding extra repo file: %s.repo' % xrepo) + options = dict(cd_conf.items(xrepo)) + try: + distro.repo_install( + distro, + xrepo, + options.pop('baseurl'), + options.pop('gpgkey'), + components=components, + **options + ) + except KeyError as err: + raise RuntimeError('missing required key: %s in config section: %s' % (err, xrepo)) + + +def install_repo(args): + """ + For a user that only wants to install the repository only (and avoid + installing Ceph and its dependencies). + """ + cd_conf = getattr(args, 'cd_conf', None) + + for hostname in args.host: + LOG.debug('Detecting platform for host %s ...', hostname) + distro = hosts.get( + hostname, + username=args.username, + # XXX this should get removed once Ceph packages are split for + # upstream. If default_release is True, it means that the user is + # trying to install on a RHEL machine and should expect to get RHEL + # packages. Otherwise, it will need to specify either a specific + # version, or repo, or a development branch. Other distro users should + # not see any differences. + use_rhceph=args.default_release, + ) + rlogger = logging.getLogger(hostname) + + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + custom_repo(distro, args, cd_conf, rlogger, install_ceph=False) + +def remove(args, purge): + LOG.info('note that some dependencies *will not* be removed because they can cause issues with qemu-kvm') + LOG.info('like: librbd1 and librados2') + remove_action = 'Uninstalling' + if purge: + remove_action = 'Purging' + LOG.debug( + '%s on cluster %s hosts %s', + remove_action, + args.cluster, + ' '.join(args.host), + ) + + for hostname in args.host: + LOG.debug('Detecting platform for host %s ...', hostname) + + distro = hosts.get( + hostname, + username=args.username, + use_rhceph=True) + LOG.info('Distro info: %s %s %s', distro.name, distro.release, distro.codename) + rlogger = logging.getLogger(hostname) + rlogger.info('%s Ceph on %s' % (remove_action, hostname)) + distro.uninstall(distro, purge=purge) + distro.conn.exit() + +def uninstall(args): + remove(args, False) + +def purge(args): + remove(args, True) + +def purgedata(args): + LOG.debug( + 'Purging data from cluster %s hosts %s', + args.cluster, + ' '.join(args.host), + ) + + installed_hosts = [] + for hostname in args.host: + distro = hosts.get(hostname, username=args.username) + ceph_is_installed = distro.conn.remote_module.which('ceph') + if ceph_is_installed: + installed_hosts.append(hostname) + distro.conn.exit() + + if installed_hosts: + LOG.error("Ceph is still installed on: %s", installed_hosts) + raise RuntimeError("refusing to purge data while Ceph is still installed") + + for hostname in args.host: + distro = hosts.get(hostname, username=args.username) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + rlogger = logging.getLogger(hostname) + rlogger.info('purging data on %s' % hostname) + + # Try to remove the contents of /var/lib/ceph first, don't worry + # about errors here, we deal with them later on + remoto.process.check( + distro.conn, + [ + 'rm', '-rf', '--one-file-system', '--', '/var/lib/ceph', + ] + ) + + # If we failed in the previous call, then we probably have OSDs + # still mounted, so we unmount them here + if distro.conn.remote_module.path_exists('/var/lib/ceph'): + rlogger.warning( + 'OSDs may still be mounted, trying to unmount them' + ) + remoto.process.run( + distro.conn, + [ + 'find', '/var/lib/ceph', + '-mindepth', '1', + '-maxdepth', '2', + '-type', 'd', + '-exec', 'umount', '{}', ';', + ] + ) + + # And now we try again to remove the contents, since OSDs should be + # unmounted, but this time we do check for errors + remoto.process.run( + distro.conn, + [ + 'rm', '-rf', '--one-file-system', '--', '/var/lib/ceph', + ] + ) + + remoto.process.run( + distro.conn, + [ + 'rm', '-rf', '--one-file-system', '--', '/etc/ceph/', + ] + ) + + distro.conn.exit() + + +class StoreVersion(argparse.Action): + """ + Like ``"store"`` but also remember which one of the exclusive + options was set. + + There are three kinds of versions: stable, testing and dev. + This sets ``version_kind`` to be the right one of the above. + + This kludge essentially lets us differentiate explicitly set + values from defaults. + """ + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values) + if self.dest == 'release': + self.dest = 'stable' + namespace.version_kind = self.dest + + +@priority(20) +def make(parser): + """ + Install Ceph packages on remote hosts. + """ + + version = parser.add_mutually_exclusive_group() + + # XXX deprecated in favor of release + version.add_argument( + '--stable', + nargs='?', + action=StoreVersion, + metavar='CODENAME', + help='[DEPRECATED] install a release known as CODENAME\ + (done by default) (default: %(default)s)', + ) + + version.add_argument( + '--release', + nargs='?', + action=StoreVersion, + metavar='CODENAME', + help='install a release known as CODENAME\ + (done by default) (default: %(default)s)', + ) + + version.add_argument( + '--testing', + nargs=0, + action=StoreVersion, + help='install the latest development release', + ) + + version.add_argument( + '--dev', + nargs='?', + action=StoreVersion, + const='master', + metavar='BRANCH_OR_TAG', + help='install a bleeding edge build from Git branch\ + or tag (default: %(default)s)', + ) + parser.add_argument( + '--dev-commit', + nargs='?', + action=StoreVersion, + metavar='COMMIT', + help='install a bleeding edge build from Git commit (defaults to master branch)', + ) + + version.set_defaults( + stable=None, # XXX deprecated in favor of release + release=None, # Set the default release in sanitize_args() + dev='master', + version_kind='stable', + ) + + parser.add_argument( + '--mon', + dest='install_mon', + action='store_true', + help='install the mon component only', + ) + + parser.add_argument( + '--mgr', + dest='install_mgr', + action='store_true', + help='install the mgr component only', + ) + + parser.add_argument( + '--mds', + dest='install_mds', + action='store_true', + help='install the mds component only', + ) + + parser.add_argument( + '--rgw', + dest='install_rgw', + action='store_true', + help='install the rgw component only', + ) + + parser.add_argument( + '--osd', + dest='install_osd', + action='store_true', + help='install the osd component only', + ) + + parser.add_argument( + '--tests', + dest='install_tests', + action='store_true', + help='install the testing components', + ) + + parser.add_argument( + '--cli', '--common', + dest='install_common', + action='store_true', + help='install the common component only', + ) + + parser.add_argument( + '--all', + dest='install_all', + action='store_true', + help='install all Ceph components (mon, osd, mds, rgw) except tests. This is the default', + ) + + repo = parser.add_mutually_exclusive_group() + + repo.add_argument( + '--adjust-repos', + dest='adjust_repos', + action='store_true', + help='install packages modifying source repos', + ) + + repo.add_argument( + '--no-adjust-repos', + dest='adjust_repos', + action='store_false', + help='install packages without modifying source repos', + ) + + repo.add_argument( + '--repo', + action='store_true', + help='install repo files only (skips package installation)', + ) + + repo.set_defaults( + adjust_repos=True, + ) + + parser.add_argument( + 'host', + metavar='HOST', + nargs='+', + help='hosts to install on', + ) + + parser.add_argument( + '--local-mirror', + nargs='?', + const='PATH', + default=None, + help='Fetch packages and push them to hosts for a local repo mirror', + ) + + parser.add_argument( + '--repo-url', + nargs='?', + dest='repo_url', + help='specify a repo URL that mirrors/contains Ceph packages', + ) + + parser.add_argument( + '--gpg-url', + nargs='?', + dest='gpg_url', + help='specify a GPG key URL to be used with custom repos\ + (defaults to ceph.com)' + ) + + parser.add_argument( + '--nogpgcheck', + action='store_true', + help='install packages without gpgcheck', + ) + + parser.set_defaults( + func=install, + ) + + +@priority(80) +def make_uninstall(parser): + """ + Remove Ceph packages from remote hosts. + """ + parser.add_argument( + 'host', + metavar='HOST', + nargs='+', + help='hosts to uninstall Ceph from', + ) + parser.set_defaults( + func=uninstall, + ) + + +@priority(80) +def make_purge(parser): + """ + Remove Ceph packages from remote hosts and purge all data. + """ + parser.add_argument( + 'host', + metavar='HOST', + nargs='+', + help='hosts to purge Ceph from', + ) + parser.set_defaults( + func=purge, + ) + + +@priority(80) +def make_purge_data(parser): + """ + Purge (delete, destroy, discard, shred) any Ceph data from /var/lib/ceph + """ + parser.add_argument( + 'host', + metavar='HOST', + nargs='+', + help='hosts to purge Ceph data from', + ) + parser.set_defaults( + func=purgedata, + ) diff --git a/ceph_deploy/lib/__init__.py b/ceph_deploy/lib/__init__.py new file mode 100644 index 0000000..fefb992 --- /dev/null +++ b/ceph_deploy/lib/__init__.py @@ -0,0 +1,27 @@ +""" +This module is meant for vendorizing Python libraries. Most libraries will need +to have some ``sys.path`` alterations done unless they are doing relative +imports. + +Do **not** add anything to this module that does not represent a vendorized +library. + +Vendored libraries should go into the ``vendor`` directory and imported from +there. This is so we allow libraries that are installed normally to be imported +if the vendored module is not available. + +The import dance here is done so that all other imports throught ceph-deploy +are kept the same regardless of where the module comes from. + +The expected way to import remoto would look like this:: + + from ceph_deploy.lib import remoto + +""" + +try: + # vendored + from .vendor import remoto +except ImportError: + # normally installed + import remoto # noqa diff --git a/ceph_deploy/lib/vendor/__init__.py b/ceph_deploy/lib/vendor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ceph_deploy/mds.py b/ceph_deploy/mds.py new file mode 100644 index 0000000..58f3eb8 --- /dev/null +++ b/ceph_deploy/mds.py @@ -0,0 +1,226 @@ +import logging +import os + +from ceph_deploy import conf +from ceph_deploy import exc +from ceph_deploy import hosts +from ceph_deploy.util import system +from ceph_deploy.lib import remoto +from ceph_deploy.cliutil import priority + + +LOG = logging.getLogger(__name__) + + +def get_bootstrap_mds_key(cluster): + """ + Read the bootstrap-mds key for `cluster`. + """ + path = '{cluster}.bootstrap-mds.keyring'.format(cluster=cluster) + try: + with open(path, 'rb') as f: + return f.read() + except IOError: + raise RuntimeError('bootstrap-mds keyring not found; run \'gatherkeys\'') + + +def create_mds(distro, name, cluster, init): + conn = distro.conn + + path = '/var/lib/ceph/mds/{cluster}-{name}'.format( + cluster=cluster, + name=name + ) + + conn.remote_module.safe_mkdir(path) + + bootstrap_keyring = '/var/lib/ceph/bootstrap-mds/{cluster}.keyring'.format( + cluster=cluster + ) + + keypath = os.path.join(path, 'keyring') + + stdout, stderr, returncode = remoto.process.check( + conn, + [ + 'ceph', + '--cluster', cluster, + '--name', 'client.bootstrap-mds', + '--keyring', bootstrap_keyring, + 'auth', 'get-or-create', 'mds.{name}'.format(name=name), + 'osd', 'allow rwx', + 'mds', 'allow', + 'mon', 'allow profile mds', + '-o', + os.path.join(keypath), + ] + ) + if returncode > 0: + for line in stderr: + conn.logger.error(line) + for line in stdout: + # yes stdout as err because this is an error + conn.logger.error(line) + conn.logger.error('exit code from command was: %s' % returncode) + raise RuntimeError('could not create mds') + + conn.remote_module.touch_file(os.path.join(path, 'done')) + conn.remote_module.touch_file(os.path.join(path, init)) + + if init == 'upstart': + remoto.process.run( + conn, + [ + 'initctl', + 'emit', + 'ceph-mds', + 'cluster={cluster}'.format(cluster=cluster), + 'id={name}'.format(name=name), + ], + timeout=7 + ) + elif init == 'sysvinit': + remoto.process.run( + conn, + [ + 'service', + 'ceph', + 'start', + 'mds.{name}'.format(name=name), + ], + timeout=7 + ) + if distro.is_el: + system.enable_service(distro.conn) + elif init == 'systemd': + remoto.process.run( + conn, + [ + 'systemctl', + 'enable', + 'ceph-mds@{name}'.format(name=name), + ], + timeout=7 + ) + remoto.process.run( + conn, + [ + 'systemctl', + 'start', + 'ceph-mds@{name}'.format(name=name), + ], + timeout=7 + ) + remoto.process.run( + conn, + [ + 'systemctl', + 'enable', + 'ceph.target', + ], + timeout=7 + ) + + + +def mds_create(args): + conf_data = conf.ceph.load_raw(args) + LOG.debug( + 'Deploying mds, cluster %s hosts %s', + args.cluster, + ' '.join(':'.join(x or '' for x in t) for t in args.mds), + ) + + key = get_bootstrap_mds_key(cluster=args.cluster) + + bootstrapped = set() + errors = 0 + failed_on_rhel = False + + for hostname, name in args.mds: + try: + distro = None + distro = hosts.get(hostname, username=args.username) + rlogger = distro.conn.logger + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + LOG.debug('remote host will use %s', distro.init) + + if hostname not in bootstrapped: + bootstrapped.add(hostname) + LOG.debug('deploying mds bootstrap to %s', hostname) + distro.conn.remote_module.write_conf( + args.cluster, + conf_data, + args.overwrite_conf, + ) + + path = '/var/lib/ceph/bootstrap-mds/{cluster}.keyring'.format( + cluster=args.cluster, + ) + + if not distro.conn.remote_module.path_exists(path): + rlogger.warning('mds keyring does not exist yet, creating one') + distro.conn.remote_module.write_keyring(path, key) + + create_mds(distro, name, args.cluster, distro.init) + distro.conn.exit() + except RuntimeError as e: + if distro and distro.normalized_name == 'redhat': + LOG.error('this feature may not yet available for %s %s' % (distro.name, distro.release)) + failed_on_rhel = True + LOG.error(e) + errors += 1 + + if errors: + if failed_on_rhel: + # because users only read the last few lines :( + LOG.error( + 'RHEL RHCS systems do not have the ability to deploy MDS yet' + ) + + raise exc.GenericError('Failed to create %d MDSs' % errors) + + +def mds(args): + if args.subcommand == 'create': + mds_create(args) + else: + LOG.error('subcommand %s not implemented', args.subcommand) + + +def colon_separated(s): + host = s + name = s + if s.count(':') == 1: + (host, name) = s.split(':') + return (host, name) + + +@priority(30) +def make(parser): + """ + Ceph MDS daemon management + """ + mds_parser = parser.add_subparsers(dest='subcommand') + mds_parser.required = True + + mds_create = mds_parser.add_parser( + 'create', + help='Deploy Ceph MDS on remote host(s)' + ) + mds_create.add_argument( + 'mds', + metavar='HOST[:NAME]', + nargs='+', + type=colon_separated, + help='host (and optionally the daemon name) to deploy on', + ) + parser.set_defaults( + func=mds, + ) diff --git a/ceph_deploy/mgr.py b/ceph_deploy/mgr.py new file mode 100644 index 0000000..6d5ad13 --- /dev/null +++ b/ceph_deploy/mgr.py @@ -0,0 +1,226 @@ +import logging +import os + +from ceph_deploy import conf +from ceph_deploy import exc +from ceph_deploy import hosts +from ceph_deploy.util import system +from ceph_deploy.lib import remoto +from ceph_deploy.cliutil import priority + + +LOG = logging.getLogger(__name__) + + +def get_bootstrap_mgr_key(cluster): + """ + Read the bootstrap-mgr key for `cluster`. + """ + path = '{cluster}.bootstrap-mgr.keyring'.format(cluster=cluster) + try: + with open(path, 'rb') as f: + return f.read() + except IOError: + raise RuntimeError('bootstrap-mgr keyring not found; run \'gatherkeys\'') + + +def create_mgr(distro, name, cluster, init): + conn = distro.conn + + path = '/var/lib/ceph/mgr/{cluster}-{name}'.format( + cluster=cluster, + name=name + ) + + conn.remote_module.safe_makedirs(path) + + bootstrap_keyring = '/var/lib/ceph/bootstrap-mgr/{cluster}.keyring'.format( + cluster=cluster + ) + + keypath = os.path.join(path, 'keyring') + + stdout, stderr, returncode = remoto.process.check( + conn, + [ + 'ceph', + '--cluster', cluster, + '--name', 'client.bootstrap-mgr', + '--keyring', bootstrap_keyring, + 'auth', 'get-or-create', 'mgr.{name}'.format(name=name), + 'mon', 'allow profile mgr', + 'osd', 'allow *', + 'mds', 'allow *', + '-o', + os.path.join(keypath), + ] + ) + if returncode > 0: + for line in stderr: + conn.logger.error(line) + for line in stdout: + # yes stdout as err because this is an error + conn.logger.error(line) + conn.logger.error('exit code from command was: %s' % returncode) + raise RuntimeError('could not create mgr') + + conn.remote_module.touch_file(os.path.join(path, 'done')) + conn.remote_module.touch_file(os.path.join(path, init)) + + if init == 'upstart': + remoto.process.run( + conn, + [ + 'initctl', + 'emit', + 'ceph-mgr', + 'cluster={cluster}'.format(cluster=cluster), + 'id={name}'.format(name=name), + ], + timeout=7 + ) + elif init == 'sysvinit': + remoto.process.run( + conn, + [ + 'service', + 'ceph', + 'start', + 'mgr.{name}'.format(name=name), + ], + timeout=7 + ) + if distro.is_el: + system.enable_service(distro.conn) + elif init == 'systemd': + remoto.process.run( + conn, + [ + 'systemctl', + 'enable', + 'ceph-mgr@{name}'.format(name=name), + ], + timeout=7 + ) + remoto.process.run( + conn, + [ + 'systemctl', + 'start', + 'ceph-mgr@{name}'.format(name=name), + ], + timeout=7 + ) + remoto.process.run( + conn, + [ + 'systemctl', + 'enable', + 'ceph.target', + ], + timeout=7 + ) + + + +def mgr_create(args): + conf_data = conf.ceph.load_raw(args) + LOG.debug( + 'Deploying mgr, cluster %s hosts %s', + args.cluster, + ' '.join(':'.join(x or '' for x in t) for t in args.mgr), + ) + + key = get_bootstrap_mgr_key(cluster=args.cluster) + + bootstrapped = set() + errors = 0 + failed_on_rhel = False + + for hostname, name in args.mgr: + try: + distro = None + distro = hosts.get(hostname, username=args.username) + rlogger = distro.conn.logger + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + LOG.debug('remote host will use %s', distro.init) + + if hostname not in bootstrapped: + bootstrapped.add(hostname) + LOG.debug('deploying mgr bootstrap to %s', hostname) + distro.conn.remote_module.write_conf( + args.cluster, + conf_data, + args.overwrite_conf, + ) + + path = '/var/lib/ceph/bootstrap-mgr/{cluster}.keyring'.format( + cluster=args.cluster, + ) + + if not distro.conn.remote_module.path_exists(path): + rlogger.warning('mgr keyring does not exist yet, creating one') + distro.conn.remote_module.write_keyring(path, key) + + create_mgr(distro, name, args.cluster, distro.init) + distro.conn.exit() + except RuntimeError as e: + if distro and distro.normalized_name == 'redhat': + LOG.error('this feature may not yet available for %s %s' % (distro.name, distro.release)) + failed_on_rhel = True + LOG.error(e) + errors += 1 + + if errors: + if failed_on_rhel: + # because users only read the last few lines :( + LOG.error( + 'RHEL RHCS systems do not have the ability to deploy MGR yet' + ) + + raise exc.GenericError('Failed to create %d MGRs' % errors) + + +def mgr(args): + if args.subcommand == 'create': + mgr_create(args) + else: + LOG.error('subcommand %s not implemented', args.subcommand) + + +def colon_separated(s): + host = s + name = s + if s.count(':') == 1: + (host, name) = s.split(':') + return (host, name) + + +@priority(30) +def make(parser): + """ + Ceph MGR daemon management + """ + mgr_parser = parser.add_subparsers(dest='subcommand') + mgr_parser.required = True + + mgr_create = mgr_parser.add_parser( + 'create', + help='Deploy Ceph MGR on remote host(s)' + ) + mgr_create.add_argument( + 'mgr', + metavar='HOST[:NAME]', + nargs='+', + type=colon_separated, + help='host (and optionally the daemon name) to deploy on', + ) + parser.set_defaults( + func=mgr, + ) diff --git a/ceph_deploy/misc.py b/ceph_deploy/misc.py new file mode 100644 index 0000000..1620e1f --- /dev/null +++ b/ceph_deploy/misc.py @@ -0,0 +1,22 @@ + +def mon_hosts(mons): + """ + Iterate through list of MON hosts, return tuples of (name, host). + """ + for m in mons: + if m.count(':'): + (name, host) = m.split(':') + else: + name = m + host = m + if name.count('.') > 0: + name = name.split('.')[0] + yield (name, host) + +def remote_shortname(socket): + """ + Obtains remote hostname of the socket and cuts off the domain part + of its FQDN. + """ + return socket.gethostname().split('.', 1)[0] + diff --git a/ceph_deploy/mon.py b/ceph_deploy/mon.py new file mode 100644 index 0000000..cd883b5 --- /dev/null +++ b/ceph_deploy/mon.py @@ -0,0 +1,596 @@ +import json +import logging +import re +import os +import time + +from ceph_deploy import conf, exc, admin +from ceph_deploy.cliutil import priority +from ceph_deploy.util.help_formatters import ToggleRawTextHelpFormatter +from ceph_deploy.util import paths, net, files, packages, system +from ceph_deploy.lib import remoto +from ceph_deploy.new import new_mon_keyring +from ceph_deploy import hosts +from ceph_deploy.misc import mon_hosts +from ceph_deploy import gatherkeys + + +LOG = logging.getLogger(__name__) + + +def mon_status_check(conn, logger, hostname, args): + """ + A direct check for JSON output on the monitor status. + + For newer versions of Ceph (dumpling and newer) a new mon_status command + was added ( `ceph daemon mon mon_status` ) and should be revisited if the + output changes as this check depends on that availability. + + """ + asok_path = paths.mon.asok(args.cluster, hostname) + + out, err, code = remoto.process.check( + conn, + [ + 'ceph', + '--cluster={cluster}'.format(cluster=args.cluster), + '--admin-daemon', + asok_path, + 'mon_status', + ], + ) + + for line in err: + logger.error(line) + + try: + return json.loads(''.join(out)) + except ValueError: + return {} + + +def catch_mon_errors(conn, logger, hostname, cfg, args): + """ + Make sure we are able to catch up common mishaps with monitors + and use that state of a monitor to determine what is missing + and warn apropriately about it. + """ + monmap = mon_status_check(conn, logger, hostname, args).get('monmap', {}) + mon_initial_members = get_mon_initial_members(args, _cfg=cfg) + public_addr = cfg.safe_get('global', 'public_addr') + public_network = cfg.safe_get('global', 'public_network') + mon_in_monmap = [ + mon.get('name') + for mon in monmap.get('mons', [{}]) + if mon.get('name') == hostname + ] + if mon_initial_members is None or not hostname in mon_initial_members: + logger.warning('%s is not defined in `mon initial members`', hostname) + if not mon_in_monmap: + logger.warning('monitor %s does not exist in monmap', hostname) + if not public_addr and not public_network: + logger.warning('neither `public_addr` nor `public_network` keys are defined for monitors') + logger.warning('monitors may not be able to form quorum') + + +def mon_status(conn, logger, hostname, args, silent=False): + """ + run ``ceph daemon mon.`hostname` mon_status`` on the remote end and provide + not only the output, but be able to return a boolean status of what is + going on. + ``False`` represents a monitor that is not doing OK even if it is up and + running, while ``True`` would mean the monitor is up and running correctly. + """ + mon = 'mon.%s' % hostname + + try: + out = mon_status_check(conn, logger, hostname, args) + if not out: + logger.warning('monitor: %s, might not be running yet' % mon) + return False + + if not silent: + logger.debug('*'*80) + logger.debug('status for monitor: %s' % mon) + for line in json.dumps(out, indent=2, sort_keys=True).split('\n'): + logger.debug(line) + logger.debug('*'*80) + if out['rank'] >= 0: + logger.info('monitor: %s is running' % mon) + return True + if out['rank'] == -1 and out['state']: + logger.info('monitor: %s is currently at the state of %s' % (mon, out['state'])) + return True + logger.info('monitor: %s is not running' % mon) + return False + except RuntimeError: + logger.info('monitor: %s is not running' % mon) + return False + + +def keyring_parser(path): + """ + This is a very, very, dumb parser that will look for `[entity]` sections + and return a list of those sections. It is not possible to parse this with + ConfigParser even though it is almost the same thing. + + Since this is only used to spit out warnings, it is OK to just be naive + about the parsing. + """ + sections = [] + with open(path) as keyring: + lines = keyring.readlines() + for line in lines: + line = line.strip('\n') + if line.startswith('[') and line.endswith(']'): + sections.append(line.strip('[]')) + return sections + + +def concatenate_keyrings(args): + """ + A helper to collect all keyrings into a single blob that will be + used to inject it to mons with ``--mkfs`` on remote nodes + + We require all keyring files to be concatenated to be in a directory + to end with ``.keyring``. + """ + keyring_path = os.path.abspath(args.keyrings) + LOG.info('concatenating keyrings from %s' % keyring_path) + LOG.info('to seed remote monitors') + + keyrings = [ + os.path.join(keyring_path, f) for f in os.listdir(keyring_path) + if os.path.isfile(os.path.join(keyring_path, f)) and f.endswith('.keyring') + ] + + contents = [] + seen_sections = {} + + if not keyrings: + path_from_arg = os.path.abspath(args.keyrings) + raise RuntimeError('could not find any keyrings in %s' % path_from_arg) + + for keyring in keyrings: + path = os.path.abspath(keyring) + + for section in keyring_parser(path): + if not seen_sections.get(section): + seen_sections[section] = path + LOG.info('adding entity "%s" from keyring %s' % (section, path)) + with open(path) as k: + contents.append(k.read()) + else: + LOG.warning('will not add keyring: %s' % path) + LOG.warning('entity "%s" from keyring %s is a duplicate' % (section, path)) + LOG.warning('already present in keyring: %s' % seen_sections[section]) + + return ''.join(contents) + + +def mon_add(args): + cfg = conf.ceph.load(args) + + # args.mon is a list with only one entry + mon_host = args.mon[0] + + try: + with open('{cluster}.mon.keyring'.format(cluster=args.cluster), + 'rb') as f: + monitor_keyring = f.read() + except IOError: + raise RuntimeError( + 'mon keyring not found; run \'new\' to create a new cluster' + ) + + LOG.info('ensuring configuration of new mon host: %s', mon_host) + args.client = args.mon + admin.admin(args) + LOG.debug( + 'Adding mon to cluster %s, host %s', + args.cluster, + mon_host, + ) + + mon_section = 'mon.%s' % mon_host + cfg_mon_addr = cfg.safe_get(mon_section, 'mon addr') + + if args.address: + LOG.debug('using mon address via --address %s' % args.address) + mon_ip = args.address + elif cfg_mon_addr: + LOG.debug('using mon address via configuration: %s' % cfg_mon_addr) + mon_ip = cfg_mon_addr + else: + mon_ip = net.get_nonlocal_ip(mon_host) + LOG.debug('using mon address by resolving host: %s' % mon_ip) + + try: + LOG.debug('detecting platform for host %s ...', mon_host) + distro = hosts.get( + mon_host, + username=args.username, + callbacks=[packages.ceph_is_installed] + ) + LOG.info('distro info: %s %s %s', distro.name, distro.release, distro.codename) + rlogger = logging.getLogger(mon_host) + + # ensure remote hostname is good to go + hostname_is_compatible(distro.conn, rlogger, mon_host) + rlogger.debug('adding mon to %s', mon_host) + args.address = mon_ip + distro.mon.add(distro, args, monitor_keyring) + + # tell me the status of the deployed mon + time.sleep(2) # give some room to start + catch_mon_errors(distro.conn, rlogger, mon_host, cfg, args) + mon_status(distro.conn, rlogger, mon_host, args) + distro.conn.exit() + + except RuntimeError as e: + LOG.error(e) + raise exc.GenericError('Failed to add monitor to host: %s' % mon_host) + + +def mon_create(args): + + cfg = conf.ceph.load(args) + if not args.mon: + args.mon = get_mon_initial_members(args, error_on_empty=True, _cfg=cfg) + + if args.keyrings: + monitor_keyring = concatenate_keyrings(args) + else: + keyring_path = '{cluster}.mon.keyring'.format(cluster=args.cluster) + try: + monitor_keyring = files.read_file(keyring_path) + except IOError: + LOG.warning('keyring (%s) not found, creating a new one' % keyring_path) + new_mon_keyring(args) + monitor_keyring = files.read_file(keyring_path) + + LOG.debug( + 'Deploying mon, cluster %s hosts %s', + args.cluster, + ' '.join(args.mon), + ) + + errors = 0 + for (name, host) in mon_hosts(args.mon): + try: + # TODO add_bootstrap_peer_hint + LOG.debug('detecting platform for host %s ...', name) + distro = hosts.get( + host, + username=args.username, + callbacks=[packages.ceph_is_installed] + ) + LOG.info('distro info: %s %s %s', distro.name, distro.release, distro.codename) + rlogger = logging.getLogger(name) + + # ensure remote hostname is good to go + hostname_is_compatible(distro.conn, rlogger, name) + rlogger.debug('deploying mon to %s', name) + distro.mon.create(distro, args, monitor_keyring) + + # tell me the status of the deployed mon + time.sleep(2) # give some room to start + mon_status(distro.conn, rlogger, name, args) + catch_mon_errors(distro.conn, rlogger, name, cfg, args) + distro.conn.exit() + + except RuntimeError as e: + LOG.error(e) + errors += 1 + + if errors: + raise exc.GenericError('Failed to create %d monitors' % errors) + + +def hostname_is_compatible(conn, logger, provided_hostname): + """ + Make sure that the host that we are connecting to has the same value as the + `hostname` in the remote host, otherwise mons can fail not reaching quorum. + """ + logger.debug('determining if provided host has same hostname in remote') + remote_hostname = conn.remote_module.shortname() + if remote_hostname == provided_hostname: + return + logger.warning('*'*80) + logger.warning('provided hostname must match remote hostname') + logger.warning('provided hostname: %s' % provided_hostname) + logger.warning('remote hostname: %s' % remote_hostname) + logger.warning('monitors may not reach quorum and create-keys will not complete') + logger.warning('*'*80) + + +def destroy_mon(conn, cluster, hostname): + import datetime + import time + retries = 5 + + path = paths.mon.path(cluster, hostname) + + if conn.remote_module.path_exists(path): + # remove from cluster + remoto.process.run( + conn, + [ + 'ceph', + '--cluster={cluster}'.format(cluster=cluster), + '-n', 'mon.', + '-k', '{path}/keyring'.format(path=path), + 'mon', + 'remove', + hostname, + ], + timeout=7, + ) + + # stop + if conn.remote_module.path_exists(os.path.join(path, 'upstart')) or system.is_upstart(conn): + status_args = [ + 'initctl', + 'status', + 'ceph-mon', + 'cluster={cluster}'.format(cluster=cluster), + 'id={hostname}'.format(hostname=hostname), + ] + + elif conn.remote_module.path_exists(os.path.join(path, 'sysvinit')): + status_args = [ + 'service', + 'ceph', + 'status', + 'mon.{hostname}'.format(hostname=hostname), + ] + elif system.is_systemd(conn): + status_args = [ + 'systemctl', + 'stop', + 'ceph-mon@{hostname}.service'.format(hostname=hostname), + ] + else: + raise RuntimeError('could not detect a supported init system, cannot continue') + + while retries: + conn.logger.info('polling the daemon to verify it stopped') + if is_running(conn, status_args): + time.sleep(5) + retries -= 1 + if retries <= 0: + raise RuntimeError('ceph-mon deamon did not stop') + else: + break + + # archive old monitor directory + fn = '{cluster}-{hostname}-{stamp}'.format( + hostname=hostname, + cluster=cluster, + stamp=datetime.datetime.utcnow().strftime("%Y-%m-%dZ%H:%M:%S"), + ) + + remoto.process.run( + conn, + [ + 'mkdir', + '-p', + '/var/lib/ceph/mon-removed', + ], + ) + + conn.remote_module.make_mon_removed_dir(path, fn) + + +def mon_destroy(args): + errors = 0 + for (name, host) in mon_hosts(args.mon): + try: + LOG.debug('Removing mon from %s', name) + + distro = hosts.get( + host, + username=args.username, + callbacks=[packages.ceph_is_installed] + ) + hostname = distro.conn.remote_module.shortname() + + destroy_mon( + distro.conn, + args.cluster, + hostname, + ) + distro.conn.exit() + + except RuntimeError as e: + LOG.error(e) + errors += 1 + + if errors: + raise exc.GenericError('Failed to destroy %d monitors' % errors) + + +def mon_create_initial(args): + mon_initial_members = get_mon_initial_members(args, error_on_empty=True) + + # create them normally through mon_create + args.mon = mon_initial_members + mon_create(args) + + # make the sets to be able to compare late + mon_in_quorum = set([]) + mon_members = set([host for host in mon_initial_members]) + + for host in mon_initial_members: + mon_name = 'mon.%s' % host + LOG.info('processing monitor %s', mon_name) + sleeps = [20, 20, 15, 10, 10, 5] + tries = 5 + rlogger = logging.getLogger(host) + distro = hosts.get( + host, + username=args.username, + callbacks=[packages.ceph_is_installed] + ) + + while tries: + status = mon_status_check(distro.conn, rlogger, host, args) + has_reached_quorum = status.get('state', '') in ['peon', 'leader'] + if not has_reached_quorum: + LOG.warning('%s monitor is not yet in quorum, tries left: %s' % (mon_name, tries)) + tries -= 1 + sleep_seconds = sleeps.pop() + LOG.warning('waiting %s seconds before retrying', sleep_seconds) + time.sleep(sleep_seconds) # Magic number + else: + mon_in_quorum.add(host) + LOG.info('%s monitor has reached quorum!', mon_name) + break + distro.conn.exit() + + if mon_in_quorum == mon_members: + LOG.info('all initial monitors are running and have formed quorum') + LOG.info('Running gatherkeys...') + gatherkeys.gatherkeys(args) + else: + LOG.error('Some monitors have still not reached quorum:') + for host in mon_members - mon_in_quorum: + LOG.error('%s', host) + raise SystemExit('cluster may not be in a healthy state') + + +def mon(args): + if args.subcommand == 'create': + mon_create(args) + elif args.subcommand == 'add': + mon_add(args) + elif args.subcommand == 'destroy': + mon_destroy(args) + elif args.subcommand == 'create-initial': + mon_create_initial(args) + else: + LOG.error('subcommand %s not implemented', args.subcommand) + + +@priority(30) +def make(parser): + """ + Ceph MON Daemon management + """ + parser.formatter_class = ToggleRawTextHelpFormatter + + mon_parser = parser.add_subparsers(dest='subcommand') + mon_parser.required = True + + mon_add = mon_parser.add_parser( + 'add', + help=('R|Add a monitor to an existing cluster:\n' + '\tceph-deploy mon add node1\n' + 'Or:\n' + '\tceph-deploy mon add --address 192.168.1.10 node1\n' + 'If the section for the monitor exists and defines a `mon addr` that\n' + 'will be used, otherwise it will fallback by resolving the hostname to an\n' + 'IP. If `--address` is used it will override all other options.') + ) + mon_add.add_argument( + '--address', + nargs='?', + ) + mon_add.add_argument( + 'mon', + nargs=1, + ) + + mon_create = mon_parser.add_parser( + 'create', + help=('R|Deploy monitors by specifying them like:\n' + '\tceph-deploy mon create node1 node2 node3\n' + 'If no hosts are passed it will default to use the\n' + '`mon initial members` defined in the configuration.') + ) + mon_create.add_argument( + '--keyrings', + nargs='?', + help='concatenate multiple keyrings to be seeded on new monitors', + ) + mon_create.add_argument( + 'mon', + nargs='*', + ) + + mon_create_initial = mon_parser.add_parser( + 'create-initial', + help=('Will deploy for monitors defined in `mon initial members`, ' + 'wait until they form quorum and then gatherkeys, reporting ' + 'the monitor status along the process. If monitors don\'t form ' + 'quorum the command will eventually time out.') + ) + mon_create_initial.add_argument( + '--keyrings', + nargs='?', + help='concatenate multiple keyrings to be seeded on new monitors', + ) + + mon_destroy = mon_parser.add_parser( + 'destroy', + help='Completely remove Ceph MON from remote host(s)' + ) + mon_destroy.add_argument( + 'mon', + nargs='+', + ) + + parser.set_defaults( + func=mon, + ) + +# +# Helpers +# + + +def get_mon_initial_members(args, error_on_empty=False, _cfg=None): + """ + Read the Ceph config file and return the value of mon_initial_members + Optionally, a NeedHostError can be raised if the value is None. + """ + if _cfg: + cfg = _cfg + else: + cfg = conf.ceph.load(args) + mon_initial_members = cfg.safe_get('global', 'mon_initial_members') + if not mon_initial_members: + if error_on_empty: + raise exc.NeedHostError( + 'could not find `mon initial members` defined in ceph.conf' + ) + else: + mon_initial_members = re.split(r'[,\s]+', mon_initial_members) + return mon_initial_members + + +def is_running(conn, args): + """ + Run a command to check the status of a mon, return a boolean. + + We heavily depend on the format of the output, if that ever changes + we need to modify this. + Check daemon status for 3 times + output of the status should be similar to:: + + mon.mira094: running {"version":"0.61.5"} + + or when it fails:: + + mon.mira094: dead {"version":"0.61.5"} + mon.mira094: not running {"version":"0.61.5"} + """ + stdout, stderr, _ = remoto.process.check( + conn, + args + ) + result_string = ' '.join(stdout) + for run_check in [': running', b' start/running']: + if run_check in result_string: + return True + return False diff --git a/ceph_deploy/new.py b/ceph_deploy/new.py new file mode 100644 index 0000000..842117b --- /dev/null +++ b/ceph_deploy/new.py @@ -0,0 +1,276 @@ +import errno +import logging +import os +import uuid +import struct +import time +import base64 +import socket + +from ceph_deploy.cliutil import priority +from ceph_deploy import conf, hosts, exc +from ceph_deploy.util import arg_validators, ssh, net +from ceph_deploy.misc import mon_hosts +from ceph_deploy.lib import remoto +from ceph_deploy.connection import get_local_connection + + +LOG = logging.getLogger(__name__) + + +def generate_auth_key(): + key = os.urandom(16) + header = struct.pack( + ' up_osds: + difference = osds - up_osds + logger.warning('there %s %d OSD%s down' % ( + ['is', 'are'][difference != 1], + difference, + "s"[difference == 1:]) + ) + + if osds > in_osds: + difference = osds - in_osds + logger.warning('there %s %d OSD%s out' % ( + ['is', 'are'][difference != 1], + difference, + "s"[difference == 1:]) + ) + + if full: + logger.warning('OSDs are full!') + + if nearfull: + logger.warning('OSDs are near full!') + + +def create_osd( + conn, + cluster, + data, + journal, + zap, + fs_type, + dmcrypt, + dmcrypt_dir, + storetype, + block_wal, + block_db, + **kw): + """ + Run on osd node, creates an OSD from a data disk. + """ + ceph_volume_executable = system.executable_path(conn, 'ceph-volume') + args = [ + ceph_volume_executable, + '--cluster', cluster, + 'lvm', + 'create', + '--%s' % storetype, + '--data', data + ] + if zap: + LOG.warning('zapping is no longer supported when preparing') + if dmcrypt: + args.append('--dmcrypt') + # TODO: re-enable dmcrypt support once ceph-volume grows it + LOG.warning('dmcrypt is currently not supported') + + if storetype == 'bluestore': + if block_wal: + args.append('--block.wal') + args.append(block_wal) + if block_db: + args.append('--block.db') + args.append(block_db) + elif storetype == 'filestore': + if not journal: + raise RuntimeError('A journal lv or GPT partition must be specified when using filestore') + args.append('--journal') + args.append(journal) + + if kw.get('debug'): + remoto.process.run( + conn, + args, + extend_env={'CEPH_VOLUME_DEBUG': '1'} + ) + + else: + remoto.process.run( + conn, + args + ) + + +def create(args, cfg, create=False): + if not args.host: + raise RuntimeError('Required host was not specified as a positional argument') + LOG.debug( + 'Creating OSD on cluster %s with data device %s', + args.cluster, + args.data + ) + + key = get_bootstrap_osd_key(cluster=args.cluster) + + bootstrapped = set() + errors = 0 + hostname = args.host + + try: + if args.data is None: + raise exc.NeedDiskError(hostname) + + distro = hosts.get( + hostname, + username=args.username, + callbacks=[packages.ceph_is_installed] + ) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + if hostname not in bootstrapped: + bootstrapped.add(hostname) + LOG.debug('Deploying osd to %s', hostname) + + conf_data = conf.ceph.load_raw(args) + distro.conn.remote_module.write_conf( + args.cluster, + conf_data, + args.overwrite_conf + ) + + create_osd_keyring(distro.conn, args.cluster, key) + + # default to bluestore unless explicitly told not to + storetype = 'bluestore' + if args.filestore: + storetype = 'filestore' + + create_osd( + distro.conn, + cluster=args.cluster, + data=args.data, + journal=args.journal, + zap=args.zap_disk, + fs_type=args.fs_type, + dmcrypt=args.dmcrypt, + dmcrypt_dir=args.dmcrypt_key_dir, + storetype=storetype, + block_wal=args.block_wal, + block_db=args.block_db, + debug=args.debug, + ) + + # give the OSD a few seconds to start + time.sleep(5) + catch_osd_errors(distro.conn, distro.conn.logger, args) + LOG.debug('Host %s is now ready for osd use.', hostname) + distro.conn.exit() + + except RuntimeError as e: + LOG.error(e) + errors += 1 + + if errors: + raise exc.GenericError('Failed to create %d OSDs' % errors) + + +def disk_zap(args): + + hostname = args.host + for disk in args.disk: + if not disk or not hostname: + raise RuntimeError('zap command needs both HOSTNAME and DISK but got "%s %s"' % (hostname, disk)) + LOG.debug('zapping %s on %s', disk, hostname) + distro = hosts.get( + hostname, + username=args.username, + callbacks=[packages.ceph_is_installed] + ) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + distro.conn.remote_module.zeroing(disk) + + ceph_volume_executable = system.executable_path(distro.conn, 'ceph-volume') + if args.debug: + remoto.process.run( + distro.conn, + [ + ceph_volume_executable, + 'lvm', + 'zap', + disk, + ], + env={'CEPH_VOLUME_DEBUG': '1'} + ) + else: + remoto.process.run( + distro.conn, + [ + ceph_volume_executable, + 'lvm', + 'zap', + disk, + ], + ) + + distro.conn.exit() + + +def disk_list(args, cfg): + command = ['fdisk', '-l'] + + for hostname in args.host: + distro = hosts.get( + hostname, + username=args.username, + callbacks=[packages.ceph_is_installed] + ) + out, err, code = remoto.process.check( + distro.conn, + command, + ) + for line in out: + line = line.decode('utf-8') + if line.startswith('Disk /'): + distro.conn.logger.info(line) + + +def osd_list(args, cfg): + for hostname in args.host: + distro = hosts.get( + hostname, + username=args.username, + callbacks=[packages.ceph_is_installed] + ) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + LOG.debug('Listing disks on {hostname}...'.format(hostname=hostname)) + ceph_volume_executable = system.executable_path(distro.conn, 'ceph-volume') + if args.debug: + remoto.process.run( + distro.conn, + [ + ceph_volume_executable, + 'lvm', + 'list', + ], + env={'CEPH_VOLUME_DEBUG': '1'} + + ) + else: + remoto.process.run( + distro.conn, + [ + ceph_volume_executable, + 'lvm', + 'list', + ], + ) + distro.conn.exit() + + +def osd(args): + cfg = conf.ceph.load(args) + + if args.subcommand == 'list': + osd_list(args, cfg) + elif args.subcommand == 'create': + create(args, cfg) + else: + LOG.error('subcommand %s not implemented', args.subcommand) + sys.exit(1) + + +def disk(args): + cfg = conf.ceph.load(args) + + if args.subcommand == 'list': + disk_list(args, cfg) + elif args.subcommand == 'create': + create(args, cfg) + elif args.subcommand == 'zap': + disk_zap(args) + else: + LOG.error('subcommand %s not implemented', args.subcommand) + sys.exit(1) + + +@priority(50) +def make(parser): + """ + Prepare a data disk on remote host. + """ + sub_command_help = dedent(""" + Create OSDs from a data disk on a remote host: + + ceph-deploy osd create {node} --data /path/to/device + + For bluestore, optional devices can be used:: + + ceph-deploy osd create {node} --data /path/to/data --block-db /path/to/db-device + ceph-deploy osd create {node} --data /path/to/data --block-wal /path/to/wal-device + ceph-deploy osd create {node} --data /path/to/data --block-db /path/to/db-device --block-wal /path/to/wal-device + + For filestore, the journal must be specified, as well as the objectstore:: + + ceph-deploy osd create {node} --filestore --data /path/to/data --journal /path/to/journal + + For data devices, it can be an existing logical volume in the format of: + vg/lv, or a device. For other OSD components like wal, db, and journal, it + can be logical volume (in vg/lv format) or it must be a GPT partition. + """ + ) + parser.formatter_class = argparse.RawDescriptionHelpFormatter + parser.description = sub_command_help + + osd_parser = parser.add_subparsers(dest='subcommand') + osd_parser.required = True + + osd_list = osd_parser.add_parser( + 'list', + help='List OSD info from remote host(s)' + ) + osd_list.add_argument( + 'host', + nargs='+', + metavar='HOST', + help='remote host(s) to list OSDs from' + ) + osd_list.add_argument( + '--debug', + action='store_true', + help='Enable debug mode on remote ceph-volume calls', + ) + osd_create = osd_parser.add_parser( + 'create', + help='Create new Ceph OSD daemon by preparing and activating a device' + ) + osd_create.add_argument( + '--data', + metavar='DATA', + help='The OSD data logical volume (vg/lv) or absolute path to device' + ) + osd_create.add_argument( + '--journal', + help='Logical Volume (vg/lv) or path to GPT partition', + ) + osd_create.add_argument( + '--zap-disk', + action='store_true', + help='DEPRECATED - cannot zap when creating an OSD' + ) + osd_create.add_argument( + '--fs-type', + metavar='FS_TYPE', + choices=['xfs', + 'btrfs' + ], + default='xfs', + help='filesystem to use to format DEVICE (xfs, btrfs)', + ) + osd_create.add_argument( + '--dmcrypt', + action='store_true', + help='use dm-crypt on DEVICE', + ) + osd_create.add_argument( + '--dmcrypt-key-dir', + metavar='KEYDIR', + default='/etc/ceph/dmcrypt-keys', + help='directory where dm-crypt keys are stored', + ) + osd_create.add_argument( + '--filestore', + action='store_true', default=None, + help='filestore objectstore', + ) + osd_create.add_argument( + '--bluestore', + action='store_true', default=None, + help='bluestore objectstore', + ) + osd_create.add_argument( + '--block-db', + default=None, + help='bluestore block.db path' + ) + osd_create.add_argument( + '--block-wal', + default=None, + help='bluestore block.wal path' + ) + osd_create.add_argument( + 'host', + nargs='?', + metavar='HOST', + help='Remote host to connect' + ) + osd_create.add_argument( + '--debug', + action='store_true', + help='Enable debug mode on remote ceph-volume calls', + ) + parser.set_defaults( + func=osd, + ) + + +@priority(50) +def make_disk(parser): + """ + Manage disks on a remote host. + """ + disk_parser = parser.add_subparsers(dest='subcommand') + disk_parser.required = True + + disk_zap = disk_parser.add_parser( + 'zap', + help='destroy existing data and filesystem on LV or partition', + ) + disk_zap.add_argument( + 'host', + nargs='?', + metavar='HOST', + help='Remote HOST(s) to connect' + ) + disk_zap.add_argument( + 'disk', + nargs='+', + metavar='DISK', + help='Disk(s) to zap' + ) + disk_zap.add_argument( + '--debug', + action='store_true', + help='Enable debug mode on remote ceph-volume calls', + ) + disk_list = disk_parser.add_parser( + 'list', + help='List disk info from remote host(s)' + ) + disk_list.add_argument( + 'host', + nargs='+', + metavar='HOST', + help='Remote HOST(s) to list OSDs from' + ) + disk_list.add_argument( + '--debug', + action='store_true', + help='Enable debug mode on remote ceph-volume calls', + ) + parser.set_defaults( + func=disk, + ) diff --git a/ceph_deploy/pkg.py b/ceph_deploy/pkg.py new file mode 100644 index 0000000..e40c17b --- /dev/null +++ b/ceph_deploy/pkg.py @@ -0,0 +1,86 @@ +import logging +from . import hosts + + +LOG = logging.getLogger(__name__) + + +def install(args): + packages = args.install.split(',') + for hostname in args.hosts: + distro = hosts.get(hostname, username=args.username) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + rlogger = logging.getLogger(hostname) + rlogger.info('installing packages on %s' % hostname) + # Do not timeout on package install. If you we this command to install + # e.g. ceph-selinux or some other package with long post script we can + # easily timeout in the 5 minutes that we use as a default timeout, + # turning off the timeout completely for the time we run the command + # should make this much more safe. + distro.conn.global_timeout = None + distro.packager.install(packages) + distro.conn.exit() + + +def remove(args): + packages = args.remove.split(',') + for hostname in args.hosts: + distro = hosts.get(hostname, username=args.username) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + rlogger = logging.getLogger(hostname) + rlogger.info('removing packages from %s' % hostname) + # Do not timeout on package removal. If we use this command to remove + # e.g. ceph-selinux or some other package with long post script we can + # easily timeout in the 5 minutes that we use as a default timeout, + # turning off the timeout completely for the time we run the command + # should make this much more safe. + distro.conn.global_timeout = None + distro.packager.remove(packages) + distro.conn.exit() + + +def pkg(args): + if args.install: + install(args) + elif args.remove: + remove(args) + + +def make(parser): + """ + Manage packages on remote hosts. + """ + + action = parser.add_mutually_exclusive_group() + + action.add_argument( + '--install', + metavar='PKG(s)', + help='Comma-separated package(s) to install', + ) + + action.add_argument( + '--remove', + metavar='PKG(s)', + help='Comma-separated package(s) to remove', + ) + + parser.add_argument( + 'hosts', + nargs='+', + ) + + parser.set_defaults( + func=pkg, + ) diff --git a/ceph_deploy/repo.py b/ceph_deploy/repo.py new file mode 100644 index 0000000..9fd5c35 --- /dev/null +++ b/ceph_deploy/repo.py @@ -0,0 +1,113 @@ +import os +import logging + +from ceph_deploy import hosts +from ceph_deploy.cliutil import priority + + +LOG = logging.getLogger(__name__) + + +def install_repo(distro, args, cd_conf, rlogger): + if args.repo_name in cd_conf.get_repos(): + LOG.info('will use repository %s from ceph-deploy config', args.repo_name) + options = dict(cd_conf.items(args.repo_name)) + extra_repos = cd_conf.get_list(args.repo_name, 'extra-repos') + try: + repo_url = options.pop('baseurl') + gpg_url = options.pop('gpgkey', None) + except KeyError as err: + raise RuntimeError( + 'missing required key: %s in config section: %s' % (err, args.repo_name) + ) + else: + repo_url = os.environ.get('CEPH_DEPLOY_REPO_URL') or args.repo_url + gpg_url = os.environ.get('CEPH_DEPLOY_GPG_URL') or args.gpg_url + extra_repos = [] + + repo_url = repo_url.strip('/') # Remove trailing slashes + distro.packager.add_repo( + args.repo_name, + repo_url, + gpg_url=gpg_url + ) + + for xrepo in extra_repos: + rlogger.info('adding extra repo: %s' % xrepo) + options = dict(cd_conf.items(xrepo)) + try: + repo_url = options.pop('baseurl') + gpg_url = options.pop('gpgkey', None) + except KeyError as err: + raise RuntimeError( + 'missing required key: %s in config section: %s' % (err, xrepo) + ) + distro.packager.add_repo( + args.repo_name, + repo_url, + gpg_url=gpg_url + ) + + +def repo(args): + cd_conf = getattr(args, 'cd_conf', None) + + for hostname in args.host: + LOG.debug('Detecting platform for host %s ...', hostname) + distro = hosts.get( + hostname, + username=args.username + ) + rlogger = logging.getLogger(hostname) + + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + if args.remove: + distro.packager.remove_repo(args.repo_name) + else: + install_repo(distro, args, cd_conf, rlogger) + + +@priority(70) +def make(parser): + """ + Repo definition management + """ + + parser.add_argument( + 'repo_name', + metavar='REPO-NAME', + help='Name of repo to manage. Can match an entry in cephdeploy.conf' + ) + + parser.add_argument( + '--repo-url', + help='a repo URL that mirrors/contains Ceph packages' + ) + + parser.add_argument( + '--gpg-url', + help='a GPG key URL to be used with custom repos' + ) + + parser.add_argument( + '--remove', '--delete', + action='store_true', + help='remove repo definition on remote host' + ) + + parser.add_argument( + 'host', + metavar='HOST', + nargs='+', + help='host(s) to install on' + ) + + parser.set_defaults( + func=repo + ) diff --git a/ceph_deploy/rgw.py b/ceph_deploy/rgw.py new file mode 100644 index 0000000..c6b9e0b --- /dev/null +++ b/ceph_deploy/rgw.py @@ -0,0 +1,233 @@ +import errno +import logging +import os + +from ceph_deploy import conf +from ceph_deploy import exc +from ceph_deploy import hosts +from ceph_deploy.util import system +from ceph_deploy.lib import remoto +from ceph_deploy.cliutil import priority + + +LOG = logging.getLogger(__name__) + + +def get_bootstrap_rgw_key(cluster): + """ + Read the bootstrap-rgw key for `cluster`. + """ + path = '{cluster}.bootstrap-rgw.keyring'.format(cluster=cluster) + try: + with open(path, 'rb') as f: + return f.read() + except IOError: + raise RuntimeError('bootstrap-rgw keyring not found; run \'gatherkeys\'') + + +def create_rgw(distro, name, cluster, init): + conn = distro.conn + + path = '/var/lib/ceph/radosgw/{cluster}-{name}'.format( + cluster=cluster, + name=name + ) + + conn.remote_module.safe_makedirs(path) + + bootstrap_keyring = '/var/lib/ceph/bootstrap-rgw/{cluster}.keyring'.format( + cluster=cluster + ) + + keypath = os.path.join(path, 'keyring') + + stdout, stderr, returncode = remoto.process.check( + conn, + [ + 'ceph', + '--cluster', cluster, + '--name', 'client.bootstrap-rgw', + '--keyring', bootstrap_keyring, + 'auth', 'get-or-create', 'client.{name}'.format(name=name), + 'osd', 'allow rwx', + 'mon', 'allow rw', + '-o', + os.path.join(keypath), + ] + ) + if returncode > 0 and returncode != errno.EACCES: + for line in stderr: + conn.logger.error(line) + for line in stdout: + # yes stdout as err because this is an error + conn.logger.error(line) + conn.logger.error('exit code from command was: %s' % returncode) + raise RuntimeError('could not create rgw') + + remoto.process.check( + conn, + [ + 'ceph', + '--cluster', cluster, + '--name', 'client.bootstrap-rgw', + '--keyring', bootstrap_keyring, + 'auth', 'get-or-create', 'client.{name}'.format(name=name), + 'osd', 'allow *', + 'mon', 'allow *', + '-o', + os.path.join(keypath), + ] + ) + + conn.remote_module.touch_file(os.path.join(path, 'done')) + conn.remote_module.touch_file(os.path.join(path, init)) + + if init == 'upstart': + remoto.process.run( + conn, + [ + 'initctl', + 'emit', + 'radosgw', + 'cluster={cluster}'.format(cluster=cluster), + 'id={name}'.format(name=name), + ], + timeout=7 + ) + elif init == 'sysvinit': + remoto.process.run( + conn, + [ + 'service', + 'ceph-radosgw', + 'start', + ], + timeout=7 + ) + if distro.is_el: + system.enable_service(distro.conn, service='ceph-radosgw') + elif init == 'systemd': + remoto.process.run( + conn, + [ + 'systemctl', + 'enable', + 'ceph-radosgw@{name}'.format(name=name), + ], + timeout=7 + ) + remoto.process.run( + conn, + [ + 'systemctl', + 'start', + 'ceph-radosgw@{name}'.format(name=name), + ], + timeout=7 + ) + remoto.process.run( + conn, + [ + 'systemctl', + 'enable', + 'ceph.target', + ], + timeout=7 + ) + + +def rgw_create(args): + conf_data = conf.ceph.load_raw(args) + LOG.debug( + 'Deploying rgw, cluster %s hosts %s', + args.cluster, + ' '.join(':'.join(x or '' for x in t) for t in args.rgw), + ) + + key = get_bootstrap_rgw_key(cluster=args.cluster) + + bootstrapped = set() + errors = 0 + for hostname, name in args.rgw: + try: + distro = hosts.get(hostname, username=args.username) + rlogger = distro.conn.logger + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + LOG.debug('remote host will use %s', distro.init) + + if hostname not in bootstrapped: + bootstrapped.add(hostname) + LOG.debug('deploying rgw bootstrap to %s', hostname) + distro.conn.remote_module.write_conf( + args.cluster, + conf_data, + args.overwrite_conf, + ) + + path = '/var/lib/ceph/bootstrap-rgw/{cluster}.keyring'.format( + cluster=args.cluster, + ) + + if not distro.conn.remote_module.path_exists(path): + rlogger.warning('rgw keyring does not exist yet, creating one') + distro.conn.remote_module.write_keyring(path, key) + + create_rgw(distro, name, args.cluster, distro.init) + distro.conn.exit() + LOG.info( + ('The Ceph Object Gateway (RGW) is now running on host %s and ' + 'default port %s'), + hostname, + '7480' + ) + except RuntimeError as e: + LOG.error(e) + errors += 1 + + if errors: + raise exc.GenericError('Failed to create %d RGWs' % errors) + + +def rgw(args): + if args.subcommand == 'create': + rgw_create(args) + else: + LOG.error('subcommand %s not implemented', args.subcommand) + + +def colon_separated(s): + host = s + name = s + if s.count(':') == 1: + (host, name) = s.split(':') + name = 'rgw.' + name + return (host, name) + + +@priority(30) +def make(parser): + """ + Ceph RGW daemon management + """ + rgw_parser = parser.add_subparsers(dest='subcommand') + rgw_parser.required = True + rgw_create = rgw_parser.add_parser( + 'create', + help='Create an RGW instance' + ) + rgw_create.add_argument( + 'rgw', + metavar='HOST[:NAME]', + nargs='+', + type=colon_separated, + help='host (and optionally the daemon name) to deploy on. \ + NAME is automatically prefixed with \'rgw.\'', + ) + parser.set_defaults( + func=rgw, + ) diff --git a/ceph_deploy/tests/__init__.py b/ceph_deploy/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ceph_deploy/tests/conftest.py b/ceph_deploy/tests/conftest.py new file mode 100644 index 0000000..ee7fcf1 --- /dev/null +++ b/ceph_deploy/tests/conftest.py @@ -0,0 +1,98 @@ +import logging +import os +import subprocess +import sys +import pytest + + +LOG = logging.getLogger(__name__) + + +def _prepend_path(env): + """ + Make sure the PATH contains the location where the Python binary + lives. This makes sure cli tools installed in a virtualenv work. + """ + if env is None: + env = os.environ + env = dict(env) + new = os.path.dirname(sys.executable) + path = env.get('PATH') + if path is not None: + new = new + ':' + path + env['PATH'] = new + return env + + +class CLIFailed(Exception): + """CLI tool failed""" + + def __init__(self, args, status): + self.args = args + self.status = status + + def __str__(self): + return '{doc}: {args}: exited with status {status}'.format( + doc=self.__doc__, + args=self.args, + status=self.status, + ) + + +class CLIProcess(object): + def __init__(self, **kw): + self.kw = kw + + def __enter__(self): + try: + self.p = subprocess.Popen(**self.kw) + except OSError as e: + raise AssertionError( + 'CLI tool {args!r} does not work: {err}'.format( + args=self.kw['args'], + err=e, + ), + ) + else: + return self.p + + def __exit__(self, exc_type, exc_val, exc_tb): + self.p.wait() + if self.p.returncode != 0: + err = CLIFailed( + args=self.kw['args'], + status=self.p.returncode, + ) + if exc_type is None: + # nothing else raised, so we should complain; if + # something else failed, we'll just log + raise err + else: + LOG.error(str(err)) + + +class CLITester(object): + # provide easy way for caller to access the exception class + # without importing us + Failed = CLIFailed + + def __init__(self, tmpdir): + self.tmpdir = tmpdir + + def __call__(self, **kw): + kw.setdefault('cwd', str(self.tmpdir)) + kw['env'] = _prepend_path(kw.get('env')) + kw['env']['COLUMNS'] = '80' + return CLIProcess(**kw) + + +@pytest.fixture +def cli(request, tmpdir): + """ + Test command line behavior. + """ + + # the tmpdir here will be the same value as the test function + # sees; we rely on that to let caller prepare and introspect + # any files the cli tool will read or create + return CLITester(tmpdir=tmpdir) diff --git a/ceph_deploy/tests/directory.py b/ceph_deploy/tests/directory.py new file mode 100644 index 0000000..81d3e19 --- /dev/null +++ b/ceph_deploy/tests/directory.py @@ -0,0 +1,13 @@ +import contextlib +import os + + +@contextlib.contextmanager +def directory(path): + prev = os.open('.', os.O_RDONLY | os.O_DIRECTORY) + try: + os.chdir(path) + yield + finally: + os.fchdir(prev) + os.close(prev) diff --git a/ceph_deploy/tests/fakes.py b/ceph_deploy/tests/fakes.py new file mode 100644 index 0000000..458ac6a --- /dev/null +++ b/ceph_deploy/tests/fakes.py @@ -0,0 +1,9 @@ + + +def fake_getaddrinfo(*a, **kw): + return_host = kw.get('return_host', 'host1') + return [[0,0,0,0, return_host]] + + +def fake_arg_val_hostname(self, host): + return host diff --git a/ceph_deploy/tests/parser/__init__.py b/ceph_deploy/tests/parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ceph_deploy/tests/parser/test_admin.py b/ceph_deploy/tests/parser/test_admin.py new file mode 100644 index 0000000..a86fa8e --- /dev/null +++ b/ceph_deploy/tests/parser/test_admin.py @@ -0,0 +1,33 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserAdmin(object): + + def setup(self): + self.parser = get_parser() + + def test_admin_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('admin --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy admin' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_admin_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('admin'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_admin_one_host(self): + args = self.parser.parse_args('admin host1'.split()) + assert args.client == ['host1'] + + def test_admin_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['admin'] + hostnames) + assert args.client == hostnames diff --git a/ceph_deploy/tests/parser/test_config.py b/ceph_deploy/tests/parser/test_config.py new file mode 100644 index 0000000..74ccb02 --- /dev/null +++ b/ceph_deploy/tests/parser/test_config.py @@ -0,0 +1,60 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + +SUBCMDS_WITH_ARGS = ['push', 'pull'] + + +class TestParserConfig(object): + + def setup(self): + self.parser = get_parser() + + def test_config_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('config --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy config' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + @pytest.mark.parametrize('cmd', SUBCMDS_WITH_ARGS) + def test_config_subcommands_with_args(self, cmd): + self.parser.parse_args(['config'] + ['%s' % cmd] + ['host1']) + + def test_config_invalid_subcommand(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('config bork'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + def test_config_push_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('config push'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_config_push_one_host(self): + args = self.parser.parse_args('config push host1'.split()) + assert args.client == ['host1'] + + def test_config_push_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('config push'.split() + hostnames) + assert args.client == hostnames + + def test_config_pull_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('config pull'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_config_pull_one_host(self): + args = self.parser.parse_args('config pull host1'.split()) + assert args.client == ['host1'] + + def test_config_pull_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('config pull'.split() + hostnames) + assert args.client == hostnames diff --git a/ceph_deploy/tests/parser/test_disk.py b/ceph_deploy/tests/parser/test_disk.py new file mode 100644 index 0000000..cedd858 --- /dev/null +++ b/ceph_deploy/tests/parser/test_disk.py @@ -0,0 +1,88 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + +SUBCMDS_WITH_ARGS = ['list', 'zap'] + + +class TestParserDisk(object): + + def setup(self): + self.parser = get_parser() + + def test_disk_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy disk' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + @pytest.mark.parametrize('cmd', SUBCMDS_WITH_ARGS) + def test_disk_valid_subcommands_with_args(self, cmd): + self.parser.parse_args(['disk'] + ['%s' % cmd] + ['host1']) + + def test_disk_invalid_subcommand(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk bork'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + def test_disk_list_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk list --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy disk list' in out + + def test_disk_list_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk list'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_disk_list_single_host(self): + args = self.parser.parse_args('disk list host1'.split()) + assert args.host[0] == 'host1' + assert args.debug is False + + def test_disk_list_single_host_debug(self): + args = self.parser.parse_args('disk list --debug host1'.split()) + assert args.host[0] == 'host1' + assert args.debug is True + + def test_disk_list_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('disk list'.split() + hostnames) + assert args.host == hostnames + + def test_disk_zap_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk zap --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy disk zap' in out + + def test_disk_zap_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk zap'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_disk_zap_single_host(self): + args = self.parser.parse_args('disk zap host1 /dev/sdb'.split()) + assert args.disk[0] == '/dev/sdb' + assert args.host == 'host1' + assert args.debug is False + + def test_disk_zap_multi_host(self): + host = 'host1' + disks = ['/dev/sda1', '/dev/sda2'] + args = self.parser.parse_args(['disk', 'zap', host] + disks) + assert args.disk == disks + + def test_disk_zap_debug_true(self): + args = \ + self.parser.parse_args('disk zap --debug host1 /dev/sdb'.split()) + assert args.disk[0] == '/dev/sdb' + assert args.host == 'host1' + assert args.debug is True diff --git a/ceph_deploy/tests/parser/test_gatherkeys.py b/ceph_deploy/tests/parser/test_gatherkeys.py new file mode 100644 index 0000000..1dcafcc --- /dev/null +++ b/ceph_deploy/tests/parser/test_gatherkeys.py @@ -0,0 +1,33 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserGatherKeys(object): + + def setup(self): + self.parser = get_parser() + + def test_gather_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('gatherkeys --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy gatherkeys' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_gatherkeys_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('gatherkeys'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_gatherkeys_one_host(self): + args = self.parser.parse_args('gatherkeys host1'.split()) + assert args.mon == ['host1'] + + def test_gatherkeys_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['gatherkeys'] + hostnames) + assert args.mon == hostnames diff --git a/ceph_deploy/tests/parser/test_install.py b/ceph_deploy/tests/parser/test_install.py new file mode 100644 index 0000000..cb6284e --- /dev/null +++ b/ceph_deploy/tests/parser/test_install.py @@ -0,0 +1,158 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + +COMP_FLAGS = [ + 'mon', 'mds', 'rgw', 'osd', 'common', 'all' +] + + +class TestParserInstall(object): + + def setup(self): + self.parser = get_parser() + + def test_install_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('install --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy install' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_install_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('install'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_install_one_host(self): + args = self.parser.parse_args('install host1'.split()) + assert args.host == ['host1'] + + def test_install_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['install'] + hostnames) + assert frozenset(args.host) == frozenset(hostnames) + + def test_install_release_default_is_none(self): + args = self.parser.parse_args('install host1'.split()) + assert args.release is None + assert args.version_kind == "stable" + + def test_install_release(self): + args = self.parser.parse_args('install --release hammer host1'.split()) + assert args.release == "hammer" + assert args.version_kind == "stable" + + @pytest.mark.skipif(reason="No release name sanity checking yet") + def test_install_release_bad_codename(self): + args = self.parser.parse_args('install --release cephalopod host1'.split()) + assert args.release != "cephalopod" + + def test_install_testing_default_is_none(self): + args = self.parser.parse_args('install host1'.split()) + assert args.testing is None + assert args.version_kind == "stable" + + def test_install_testing_true(self): + args = self.parser.parse_args('install --testing host1'.split()) + assert len(args.testing) == 0 + assert args.version_kind == "testing" + + def test_install_dev_disabled_by_default(self): + args = self.parser.parse_args('install host1'.split()) + # dev defaults to master, but version_kind nullifies it + assert args.dev == "master" + assert args.version_kind == "stable" + + def test_install_dev_custom_version(self): + args = self.parser.parse_args('install --dev v0.80.8 host1'.split()) + assert args.dev == "v0.80.8" + assert args.version_kind == "dev" + + @pytest.mark.skipif(reason="test reflects desire, but not code reality") + def test_install_dev_option_default_is_master(self): + # I don't think this is the way argparse works. + args = self.parser.parse_args('install --dev host1'.split()) + assert args.dev == "master" + assert args.version_kind == "dev" + + def test_install_release_testing_mutex(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('install --release hammer --testing host1'.split()) + out, err = capsys.readouterr() + assert 'not allowed with argument' in err + + def test_install_release_dev_mutex(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('install --release hammer --dev master host1'.split()) + out, err = capsys.readouterr() + assert 'not allowed with argument' in err + + def test_install_testing_dev_mutex(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('install --testing --dev master host1'.split()) + out, err = capsys.readouterr() + assert 'not allowed with argument' in err + + @pytest.mark.parametrize('comp', COMP_FLAGS) + def test_install_component_default_is_false(self, comp): + args = self.parser.parse_args('install host1'.split()) + assert getattr(args, 'install_%s' % comp) is False + + @pytest.mark.parametrize('comp', COMP_FLAGS) + def test_install_component_true(self, comp): + args = self.parser.parse_args(('install --%s host1' % comp).split()) + assert getattr(args, 'install_%s' % comp) is True + + def test_install_multi_component(self): + args = self.parser.parse_args(('install --mon --rgw host1').split()) + assert args.install_mon + assert args.install_rgw + + def test_install_adjust_repos_default_is_true(self): + args = self.parser.parse_args('install host1'.split()) + assert args.adjust_repos + + def test_install_adjust_repos_false(self): + args = self.parser.parse_args('install --no-adjust-repos host1'.split()) + assert not args.adjust_repos + + def test_install_adjust_repos_false_with_custom_release(self): + args = self.parser.parse_args('install --release firefly --no-adjust-repos host1'.split()) + assert args.release == "firefly" + assert not args.adjust_repos + + def test_install_repo_default_is_false(self): + args = self.parser.parse_args('install host1'.split()) + assert not args.repo + + def test_install_repo_true(self): + args = self.parser.parse_args('install --repo host1'.split()) + assert args.repo + + def test_install_local_mirror_default_is_none(self): + args = self.parser.parse_args('install host1'.split()) + assert args.local_mirror is None + + def test_install_local_mirror_custom_path(self): + args = self.parser.parse_args('install --local-mirror /mnt/mymirror host1'.split()) + assert args.local_mirror == "/mnt/mymirror" + + def test_install_repo_url_default_is_none(self): + args = self.parser.parse_args('install host1'.split()) + assert args.repo_url is None + + def test_install_repo_url_custom_path(self): + args = self.parser.parse_args('install --repo-url https://ceph.com host1'.split()) + assert args.repo_url == "https://ceph.com" + + def test_install_gpg_url_default_is_none(self): + args = self.parser.parse_args('install host1'.split()) + assert args.gpg_url is None + + def test_install_gpg_url_custom_path(self): + args = self.parser.parse_args('install --gpg-url https://ceph.com/key host1'.split()) + assert args.gpg_url == "https://ceph.com/key" diff --git a/ceph_deploy/tests/parser/test_main.py b/ceph_deploy/tests/parser/test_main.py new file mode 100644 index 0000000..ac7186e --- /dev/null +++ b/ceph_deploy/tests/parser/test_main.py @@ -0,0 +1,100 @@ +import pytest + +import ceph_deploy +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +SUBCMDS_WITH_ARGS = [ + 'new', 'install', 'rgw', 'mds', 'mon', 'gatherkeys', 'disk', 'osd', + 'admin', 'config', 'uninstall', 'purgedata', 'purge', 'pkg' +] +SUBCMDS_WITHOUT_ARGS = ['forgetkeys'] + + +class TestParserMain(object): + + def setup(self): + self.parser = get_parser() + + def test_verbose_true(self): + args = self.parser.parse_args('--verbose forgetkeys'.split()) + assert args.verbose + + def test_verbose_default_is_false(self): + args = self.parser.parse_args('forgetkeys'.split()) + assert not args.verbose + + def test_quiet_true(self): + args = self.parser.parse_args('--quiet forgetkeys'.split()) + assert args.quiet + + def test_quiet_default_is_false(self): + args = self.parser.parse_args('forgetkeys'.split()) + assert not args.quiet + + def test_verbose_quiet_are_mutually_exclusive(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('--verbose --quiet forgetkeys'.split()) + out, err = capsys.readouterr() + assert 'not allowed with argument' in err + + def test_version(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('--version'.split()) + out, err = capsys.readouterr() + assert ceph_deploy.__version__ in (out.strip(), err.strip()) + + def test_custom_username(self): + args = self.parser.parse_args('--username trhoden forgetkeys'.split()) + assert args.username == 'trhoden' + + def test_default_username_is_none(self): + args = self.parser.parse_args('forgetkeys'.split()) + assert args.username is None + + def test_overwrite_conf_default_false(self): + args = self.parser.parse_args('forgetkeys'.split()) + assert not args.overwrite_conf + + def test_overwrite_conf_true(self): + args = self.parser.parse_args('--overwrite-conf forgetkeys'.split()) + assert args.overwrite_conf + + def test_default_cluster_name(self): + args = self.parser.parse_args('forgetkeys'.split()) + assert args.cluster == 'ceph' + + def test_default_ceph_conf_is_none(self): + args = self.parser.parse_args('forgetkeys'.split()) + assert args.ceph_conf is None + + def test_custom_ceph_conf(self): + args = self.parser.parse_args('--ceph-conf /tmp/ceph.conf forgetkeys'.split()) + assert args.ceph_conf == '/tmp/ceph.conf' + + @pytest.mark.parametrize('cmd', SUBCMDS_WITH_ARGS) + def test_valid_subcommands_with_args(self, cmd, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args(['%s' % cmd]) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + assert 'invalid choice' not in err + + @pytest.mark.parametrize('cmd', SUBCMDS_WITHOUT_ARGS) + def test_valid_subcommands_without_args(self, cmd, capsys): + self.parser.parse_args(['%s' % cmd]) + + def test_invalid_subcommand(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('bork'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + def test_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('--help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy' in out + assert 'optional arguments:' in out + assert 'commands:' in out diff --git a/ceph_deploy/tests/parser/test_mds.py b/ceph_deploy/tests/parser/test_mds.py new file mode 100644 index 0000000..0b81c38 --- /dev/null +++ b/ceph_deploy/tests/parser/test_mds.py @@ -0,0 +1,35 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserMDS(object): + + def setup(self): + self.parser = get_parser() + + def test_mds_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mds --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy mds' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_mds_create_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mds create'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_mds_create_one_host(self): + args = self.parser.parse_args('mds create host1'.split()) + assert args.mds[0][0] == 'host1' + + def test_mds_create_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['mds', 'create'] + hostnames) + # args.mds is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.mds] + assert frozenset(hosts) == frozenset(hostnames) diff --git a/ceph_deploy/tests/parser/test_mon.py b/ceph_deploy/tests/parser/test_mon.py new file mode 100644 index 0000000..4c03101 --- /dev/null +++ b/ceph_deploy/tests/parser/test_mon.py @@ -0,0 +1,122 @@ +import pytest + +from ceph_deploy.cli import get_parser + +SUBCMDS_WITH_ARGS = ['add', 'destroy', 'create'] +SUBCMDS_WITHOUT_ARGS = ['create', 'create-initial'] + + +class TestParserMON(object): + + def setup(self): + self.parser = get_parser() + + def test_mon_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mon --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy mon' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + @pytest.mark.parametrize('cmd', SUBCMDS_WITH_ARGS) + def test_mon_valid_subcommands_with_args(self, cmd, capsys): + args = self.parser.parse_args(['mon'] + ['%s' % cmd] + ['host1']) + assert args.subcommand == cmd + + @pytest.mark.parametrize('cmd', SUBCMDS_WITHOUT_ARGS) + def test_mon_valid_subcommands_without_args(self, cmd, capsys): + args = self.parser.parse_args(['mon'] + ['%s' % cmd]) + assert args.subcommand == cmd + + def test_mon_invalid_subcommand(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mon bork'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + def test_mon_create_initial_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mon create-initial --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy mon create-initial' in out + + def test_mon_create_initial_keyrings_default_none(self): + args = self.parser.parse_args('mon create-initial'.split()) + assert args.keyrings is None + + def test_mon_create_initial_keyrings_custom_dir(self): + args = self.parser.parse_args('mon create-initial --keyrings /tmp/keys'.split()) + assert args.keyrings == "/tmp/keys" + + def test_mon_create_initial_keyrings_host_raises_err(self): + with pytest.raises(SystemExit): + self.parser.parse_args('mon create-initial test1'.split()) + + def test_mon_create_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mon create --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy mon create' in out + + def test_mon_create_keyrings_default_none(self): + args = self.parser.parse_args('mon create'.split()) + assert args.keyrings is None + + def test_mon_create_keyrings_custom_dir(self): + args = self.parser.parse_args('mon create --keyrings /tmp/keys'.split()) + assert args.keyrings == "/tmp/keys" + + def test_mon_create_single_host(self): + args = self.parser.parse_args('mon create test1'.split()) + assert args.mon == ['test1'] + + def test_mon_create_multi_host(self): + hosts = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('mon create'.split() + hosts) + assert args.mon == hosts + + def test_mon_add_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mon add --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy mon add' in out + + def test_mon_add_address_default_none(self): + args = self.parser.parse_args('mon add test1'.split()) + assert args.address is None + + def test_mon_add_address_custom_addr(self): + args = self.parser.parse_args('mon add test1 --address 10.10.0.1'.split()) + assert args.address == '10.10.0.1' + + def test_mon_add_no_host_raises_err(self): + with pytest.raises(SystemExit): + self.parser.parse_args('mon add'.split()) + + def test_mon_add_one_host_okay(self): + args = self.parser.parse_args('mon add test1'.split()) + assert args.mon == ["test1"] + + def test_mon_add_multi_host_raises_err(self): + with pytest.raises(SystemExit): + self.parser.parse_args('mon add test1 test2'.split()) + + def test_mon_destroy_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mon destroy --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy mon destroy' in out + + def test_mon_destroy_no_host_raises_err(self): + with pytest.raises(SystemExit): + self.parser.parse_args('mon destroy'.split()) + + def test_mon_destroy_one_host_okay(self): + args = self.parser.parse_args('mon destroy test1'.split()) + assert args.mon == ["test1"] + + def test_mon_destroy_multi_host(self): + hosts = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('mon destroy'.split() + hosts) + assert args.mon == hosts diff --git a/ceph_deploy/tests/parser/test_new.py b/ceph_deploy/tests/parser/test_new.py new file mode 100644 index 0000000..9395bab --- /dev/null +++ b/ceph_deploy/tests/parser/test_new.py @@ -0,0 +1,84 @@ +import pytest +from mock import patch + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.fakes import fake_arg_val_hostname +from ceph_deploy.tests.util import assert_too_few_arguments + +@patch('ceph_deploy.util.arg_validators.Hostname.__call__', fake_arg_val_hostname) +class TestParserNew(object): + + def setup(self): + self.parser = get_parser() + + def test_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('new --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy new' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_new_copykey_true_by_default(self): + args = self.parser.parse_args('new host1'.split()) + assert args.ssh_copykey + + def test_new_copykey_false(self): + args = self.parser.parse_args('new --no-ssh-copykey host1'.split()) + assert not args.ssh_copykey + + def test_new_fsid_none_by_default(self): + args = self.parser.parse_args('new host1'.split()) + assert args.fsid is None + + def test_new_fsid_custom_fsid(self): + args = self.parser.parse_args('new --fsid bc50d015-65c9-457a-bfed-e37b92756527 host1'.split()) + assert args.fsid == 'bc50d015-65c9-457a-bfed-e37b92756527' + + @pytest.mark.skipif(reason="no UUID validation yet") + def test_new_fsid_custom_fsid_bad(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('new --fsid bc50d015-65c9-457a-bfed-e37'.split()) + out, err = capsys.readouterr() + #TODO check for correct error string in err + + def test_new_networks_none_by_default(self): + args = self.parser.parse_args('new host1'.split()) + assert args.public_network is None + assert args.cluster_network is None + + def test_new_public_network_custom(self): + args = self.parser.parse_args('new --public-network 10.10.0.0/16 host1'.split()) + assert args.public_network == "10.10.0.0/16" + + def test_new_cluster_network_custom(self): + args = self.parser.parse_args('new --cluster-network 10.10.0.0/16 host1'.split()) + assert args.cluster_network == "10.10.0.0/16" + + def test_new_public_network_custom_bad(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('new --public-network 10.10.0.0'.split()) + out, err = capsys.readouterr() + assert "error: subnet must" in err + + def test_new_cluster_network_custom_bad(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('new --cluster-network 10.10.0.0'.split()) + out, err = capsys.readouterr() + assert "error: subnet must" in err + + def test_new_mon_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('new'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_new_one_mon(self): + hostnames = ['test1'] + args = self.parser.parse_args(['new'] + hostnames) + assert args.mon == hostnames + + def test_new_multiple_mons(self): + hostnames = ['test1', 'test2', 'test3'] + args = self.parser.parse_args(['new'] + hostnames) + assert frozenset(args.mon) == frozenset(hostnames) diff --git a/ceph_deploy/tests/parser/test_osd.py b/ceph_deploy/tests/parser/test_osd.py new file mode 100644 index 0000000..3b983c2 --- /dev/null +++ b/ceph_deploy/tests/parser/test_osd.py @@ -0,0 +1,101 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + +SUBCMDS_WITH_ARGS = ['list', 'create'] + + +class TestParserOSD(object): + + def setup(self): + self.parser = get_parser() + + def test_osd_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy osd' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + @pytest.mark.parametrize('cmd', SUBCMDS_WITH_ARGS) + def test_osd_valid_subcommands_with_args(self, cmd): + self.parser.parse_args(['osd'] + ['%s' % cmd] + ['host1']) + + def test_osd_invalid_subcommand(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd bork'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + def test_osd_list_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd list --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy osd list' in out + + def test_osd_list_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd list'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_osd_list_single_host(self): + args = self.parser.parse_args('osd list host1'.split()) + assert args.host[0] == 'host1' + + def test_osd_list_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('osd list'.split() + hostnames) + assert args.host == hostnames + + def test_osd_create_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd create --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy osd create' in out + + def test_osd_create_single_host(self): + args = self.parser.parse_args('osd create host1 --data /dev/sdb'.split()) + assert args.host == 'host1' + assert args.data == '/dev/sdb' + + def test_osd_create_zap_default_false(self): + args = self.parser.parse_args('osd create host1 --data /dev/sdb'.split()) + assert args.zap_disk is False + + def test_osd_create_zap_true(self): + args = self.parser.parse_args('osd create --zap-disk host1 --data /dev/sdb'.split()) + assert args.zap_disk is True + + def test_osd_create_fstype_default_xfs(self): + args = self.parser.parse_args('osd create host1 --data /dev/sdb'.split()) + assert args.fs_type == "xfs" + + def test_osd_create_fstype_btrfs(self): + args = self.parser.parse_args('osd create --fs-type btrfs host1 --data /dev/sdb'.split()) + assert args.fs_type == "btrfs" + + def test_osd_create_fstype_invalid(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd create --fs-type bork host1 --data /dev/sdb'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + def test_osd_create_dmcrypt_default_false(self): + args = self.parser.parse_args('osd create host1 --data /dev/sdb'.split()) + assert args.dmcrypt is False + + def test_osd_create_dmcrypt_true(self): + args = self.parser.parse_args('osd create --dmcrypt host1 --data /dev/sdb'.split()) + assert args.dmcrypt is True + + def test_osd_create_dmcrypt_key_dir_default(self): + args = self.parser.parse_args('osd create host1 --data /dev/sdb'.split()) + assert args.dmcrypt_key_dir == "/etc/ceph/dmcrypt-keys" + + def test_osd_create_dmcrypt_key_dir_custom(self): + args = self.parser.parse_args('osd create --dmcrypt --dmcrypt-key-dir /tmp/keys host1 --data /dev/sdb'.split()) + assert args.dmcrypt_key_dir == "/tmp/keys" + diff --git a/ceph_deploy/tests/parser/test_pkg.py b/ceph_deploy/tests/parser/test_pkg.py new file mode 100644 index 0000000..9061a68 --- /dev/null +++ b/ceph_deploy/tests/parser/test_pkg.py @@ -0,0 +1,66 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserPkg(object): + + def setup(self): + self.parser = get_parser() + + def test_pkg_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('pkg --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy pkg' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_pkg_install_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('pkg --install pkg1'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_pkg_install_one_host(self): + args = self.parser.parse_args('pkg --install pkg1 host1'.split()) + assert args.hosts == ['host1'] + assert args.install == "pkg1" + + def test_pkg_install_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('pkg --install pkg1'.split() + hostnames) + assert args.hosts == hostnames + assert args.install == "pkg1" + + def test_pkg_install_muliple_pkgs(self): + args = self.parser.parse_args('pkg --install pkg1,pkg2 host1'.split()) + assert args.install == "pkg1,pkg2" + + def test_pkg_remove_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('pkg --remove pkg1'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_pkg_remove_one_host(self): + args = self.parser.parse_args('pkg --remove pkg1 host1'.split()) + assert args.hosts == ['host1'] + assert args.remove == "pkg1" + + def test_pkg_remove_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('pkg --remove pkg1'.split() + hostnames) + assert args.hosts == hostnames + assert args.remove == "pkg1" + + def test_pkg_remove_muliple_pkgs(self): + args = self.parser.parse_args('pkg --remove pkg1,pkg2 host1'.split()) + assert args.remove == "pkg1,pkg2" + + def test_pkg_install_remove_are_mutex(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('pkg --install pkg2 --remove pkg1 host1'.split()) + out, err = capsys.readouterr() + assert "argument --remove: not allowed with argument --install" in err diff --git a/ceph_deploy/tests/parser/test_purge.py b/ceph_deploy/tests/parser/test_purge.py new file mode 100644 index 0000000..8dcb348 --- /dev/null +++ b/ceph_deploy/tests/parser/test_purge.py @@ -0,0 +1,33 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserPurge(object): + + def setup(self): + self.parser = get_parser() + + def test_purge_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('purge --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy purge' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_purge_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('purge'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_purge_one_host(self): + args = self.parser.parse_args('purge host1'.split()) + assert args.host == ['host1'] + + def test_purge_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['purge'] + hostnames) + assert frozenset(args.host) == frozenset(hostnames) diff --git a/ceph_deploy/tests/parser/test_purgedata.py b/ceph_deploy/tests/parser/test_purgedata.py new file mode 100644 index 0000000..dadef2b --- /dev/null +++ b/ceph_deploy/tests/parser/test_purgedata.py @@ -0,0 +1,33 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserPurgeData(object): + + def setup(self): + self.parser = get_parser() + + def test_purgedata_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('purgedata --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy purgedata' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_purgedata_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('purgedata'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_purgedata_one_host(self): + args = self.parser.parse_args('purgedata host1'.split()) + assert args.host == ['host1'] + + def test_purgedata_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['purgedata'] + hostnames) + assert frozenset(args.host) == frozenset(hostnames) diff --git a/ceph_deploy/tests/parser/test_repo.py b/ceph_deploy/tests/parser/test_repo.py new file mode 100644 index 0000000..a84901e --- /dev/null +++ b/ceph_deploy/tests/parser/test_repo.py @@ -0,0 +1,71 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserRepo(object): + + def setup(self): + self.parser = get_parser() + + def test_repo_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('repo --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy repo' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_repo_name_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('repo'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_repo_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('repo ceph'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_repo_one_host(self): + args = self.parser.parse_args('repo ceph host1'.split()) + assert args.host == ['host1'] + + def test_repo_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['repo', 'ceph'] + hostnames) + assert frozenset(args.host) == frozenset(hostnames) + + def test_repo_name(self): + args = self.parser.parse_args('repo ceph host1'.split()) + assert args.repo_name == 'ceph' + + def test_repo_remove_default_is_false(self): + args = self.parser.parse_args('repo ceph host1'.split()) + assert not args.remove + + def test_repo_remove_set_true(self): + args = self.parser.parse_args('repo ceph --remove host1'.split()) + assert args.remove + + def test_repo_remove_delete_alias(self): + args = self.parser.parse_args('repo ceph --delete host1'.split()) + assert args.remove + + def test_repo_url_default_is_none(self): + args = self.parser.parse_args('repo ceph host1'.split()) + assert args.repo_url is None + + def test_repo_url_custom_path(self): + args = self.parser.parse_args('repo ceph --repo-url https://ceph.com host1'.split()) + assert args.repo_url == "https://ceph.com" + + def test_repo_gpg_url_default_is_none(self): + args = self.parser.parse_args('repo ceph host1'.split()) + assert args.gpg_url is None + + def test_repo_gpg_url_custom_path(self): + args = self.parser.parse_args('repo ceph --gpg-url https://ceph.com/key host1'.split()) + assert args.gpg_url == "https://ceph.com/key" diff --git a/ceph_deploy/tests/parser/test_rgw.py b/ceph_deploy/tests/parser/test_rgw.py new file mode 100644 index 0000000..5cbf0a0 --- /dev/null +++ b/ceph_deploy/tests/parser/test_rgw.py @@ -0,0 +1,35 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserRGW(object): + + def setup(self): + self.parser = get_parser() + + def test_rgw_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('rgw --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy rgw' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_rgw_create_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('rgw create'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_rgw_create_one_host(self): + args = self.parser.parse_args('rgw create host1'.split()) + assert args.rgw[0][0] == 'host1' + + def test_rgw_create_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['rgw', 'create'] + hostnames) + # args.rgw is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.rgw] + assert frozenset(hosts) == frozenset(hostnames) diff --git a/ceph_deploy/tests/parser/test_uninstall.py b/ceph_deploy/tests/parser/test_uninstall.py new file mode 100644 index 0000000..30cd918 --- /dev/null +++ b/ceph_deploy/tests/parser/test_uninstall.py @@ -0,0 +1,33 @@ +import pytest + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.util import assert_too_few_arguments + + +class TestParserUninstall(object): + + def setup(self): + self.parser = get_parser() + + def test_uninstall_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('uninstall --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy uninstall' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_uninstall_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('uninstall'.split()) + out, err = capsys.readouterr() + assert_too_few_arguments(err) + + def test_uninstall_one_host(self): + args = self.parser.parse_args('uninstall host1'.split()) + assert args.host == ['host1'] + + def test_uninstall_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args(['uninstall'] + hostnames) + assert frozenset(args.host) == frozenset(hostnames) diff --git a/ceph_deploy/tests/test_cli_admin.py b/ceph_deploy/tests/test_cli_admin.py new file mode 100644 index 0000000..06a95dc --- /dev/null +++ b/ceph_deploy/tests/test_cli_admin.py @@ -0,0 +1,60 @@ +import os +import subprocess + +import pytest +from mock import patch, MagicMock, Mock + +from ceph_deploy.cli import _main as main +from ceph_deploy.hosts import remotes +from ceph_deploy.tests.directory import directory + + +def test_bad_no_conf(tmpdir, cli): + with pytest.raises(cli.Failed) as err: + with cli( + args=['ceph-deploy', 'admin', 'host1'], + stderr=subprocess.PIPE, + ) as p: + result = p.stderr.read().decode('utf-8') + assert 'No such file or directory: \'ceph.conf\'' in result + assert err.value.status == 1 + + +def test_bad_no_key(tmpdir, cli): + with tmpdir.join('ceph.conf').open('w'): + pass + with pytest.raises(cli.Failed) as err: + with cli( + args=['ceph-deploy', 'admin', 'host1'], + stderr=subprocess.PIPE, + ) as p: + result = p.stderr.read().decode('utf-8') + assert 'ceph.client.admin.keyring not found' in result + assert err.value.status == 1 + + +def test_write_keyring(tmpdir): + with tmpdir.join('ceph.conf').open('w'): + pass + with tmpdir.join('ceph.client.admin.keyring').open('wb'): + pass + + etc_ceph = os.path.join(str(tmpdir), 'etc', 'ceph') + os.makedirs(etc_ceph) + + distro = MagicMock() + distro.conn = MagicMock() + remotes.write_file.__defaults__ = (0o644, str(tmpdir), -1, -1) + distro.conn.remote_module = remotes + distro.conn.remote_module.write_conf = Mock() + + with patch('ceph_deploy.admin.hosts'): + with patch('ceph_deploy.admin.hosts.get', MagicMock(return_value=distro)): + with directory(str(tmpdir)): + main(args=['admin', 'host1']) + + keyring_file = os.path.join(etc_ceph, 'ceph.client.admin.keyring') + assert os.path.exists(keyring_file) + + file_mode = oct(os.stat(keyring_file).st_mode & 0o777) + assert file_mode == oct(0o600) diff --git a/ceph_deploy/tests/test_cli_mon.py b/ceph_deploy/tests/test_cli_mon.py new file mode 100644 index 0000000..9948681 --- /dev/null +++ b/ceph_deploy/tests/test_cli_mon.py @@ -0,0 +1,56 @@ +import subprocess + +import pytest +from mock import Mock, patch + +from ceph_deploy.cli import _main as main +from ceph_deploy.tests.directory import directory +from ceph_deploy.tests.util import assert_too_few_arguments + + +#TODO: This test does check that things fail if the .conf file is missing +def test_bad_no_conf(tmpdir, cli): + with pytest.raises(cli.Failed) as err: + with cli( + args=['ceph-deploy', 'mon'], + stderr=subprocess.PIPE, + ) as p: + result = p.stderr.read().decode('utf-8') + assert 'usage: ceph-deploy' in result + assert_too_few_arguments(result) + assert err.value.status == 2 + + +def make_fake_connection(platform_information=None): + get_connection = Mock() + get_connection.return_value = get_connection + get_connection.remote_module.platform_information = Mock( + return_value=platform_information) + return get_connection + + +def test_new(tmpdir, capsys): + with tmpdir.join('ceph.conf').open('w') as f: + f.write("""\ +[global] +fsid = 6ede5564-3cf1-44b5-aa96-1c77b0c3e1d0 +mon initial members = host1 +""") + + fake_ip_addresses = lambda x: ['10.0.0.1'] + try: + with patch('ceph_deploy.new.net.ip_addresses', fake_ip_addresses): + with patch('ceph_deploy.new.net.get_nonlocal_ip', lambda x: '10.0.0.1'): + with patch('ceph_deploy.new.arg_validators.Hostname', lambda: lambda x: x): + with patch('ceph_deploy.new.hosts'): + with directory(str(tmpdir)): + main(['-v', 'new', '--no-ssh-copykey', 'host1']) + except SystemExit as e: + raise AssertionError('Unexpected exit: %s', e) + out, err = capsys.readouterr() + err = err.lower() + assert 'creating new cluster named ceph' in err + assert 'monitor host1 at 10.0.0.1' in err + assert 'resolving host host1' in err + assert "monitor initial members are ['host1']" in err + assert "monitor addrs are ['10.0.0.1']" in err diff --git a/ceph_deploy/tests/test_cli_new.py b/ceph_deploy/tests/test_cli_new.py new file mode 100644 index 0000000..056bc78 --- /dev/null +++ b/ceph_deploy/tests/test_cli_new.py @@ -0,0 +1,71 @@ +import re +import uuid + +from mock import patch + +from ceph_deploy import conf +from ceph_deploy.cli import _main as main +from ceph_deploy.tests.directory import directory +import pytest + + +def test_write_global_conf_section(tmpdir): + fake_ip_addresses = lambda x: ['10.0.0.1'] + + with patch('ceph_deploy.new.hosts'): + with patch('ceph_deploy.new.net.ip_addresses', fake_ip_addresses): + with patch('ceph_deploy.new.net.get_nonlocal_ip', lambda x: '10.0.0.1'): + with patch('ceph_deploy.new.arg_validators.Hostname', lambda: lambda x: x): + with directory(str(tmpdir)): + main(args=['new', 'host1']) + with tmpdir.join('ceph.conf').open() as f: + cfg = conf.ceph.parse(f) + assert cfg.sections() == ['global'] + + +@pytest.fixture +def newcfg(request, tmpdir): + fake_ip_addresses = lambda x: ['10.0.0.1'] + + def new(*args): + with patch('ceph_deploy.new.net.ip_addresses', fake_ip_addresses): + with patch('ceph_deploy.new.hosts'): + with patch('ceph_deploy.new.net.get_nonlocal_ip', lambda x: '10.0.0.1'): + with patch('ceph_deploy.new.arg_validators.Hostname', lambda: lambda x: x): + with directory(str(tmpdir)): + main(args=['new'] + list(args)) + with tmpdir.join('ceph.conf').open() as f: + cfg = conf.ceph.parse(f) + return cfg + return new + + +def test_uuid(newcfg): + cfg = newcfg('host1') + fsid = cfg.get('global', 'fsid') + # make sure it's a valid uuid + uuid.UUID(hex=fsid) + # make sure it looks pretty, too + UUID_RE = re.compile( + r'^[0-9a-f]{8}-' + + r'[0-9a-f]{4}-' + # constant 4 here, we want to enforce randomness and not leak + # MACs or time + + r'4[0-9a-f]{3}-' + + r'[0-9a-f]{4}-' + + r'[0-9a-f]{12}$', + ) + assert UUID_RE.match(fsid) + + +def test_mons(newcfg): + cfg = newcfg('node01', 'node07', 'node34') + mon_initial_members = cfg.get('global', 'mon_initial_members') + assert mon_initial_members == 'node01, node07, node34' + + +def test_defaults(newcfg): + cfg = newcfg('host1') + assert cfg.get('global', 'auth cluster required') == 'cephx' + assert cfg.get('global', 'auth service required') == 'cephx' + assert cfg.get('global', 'auth client required') == 'cephx' diff --git a/ceph_deploy/tests/test_cli_rgw.py b/ceph_deploy/tests/test_cli_rgw.py new file mode 100644 index 0000000..b0b86c4 --- /dev/null +++ b/ceph_deploy/tests/test_cli_rgw.py @@ -0,0 +1,11 @@ +import ceph_deploy.rgw as rgw + + +def test_rgw_prefix_auto(): + daemon = rgw.colon_separated("hostname") + assert daemon == ("hostname", "rgw.hostname") + + +def test_rgw_prefix_custom(): + daemon = rgw.colon_separated("hostname:mydaemon") + assert daemon == ("hostname", "rgw.mydaemon") diff --git a/ceph_deploy/tests/test_conf.py b/ceph_deploy/tests/test_conf.py new file mode 100644 index 0000000..8d435df --- /dev/null +++ b/ceph_deploy/tests/test_conf.py @@ -0,0 +1,86 @@ +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO +from ceph_deploy import conf + + +def test_simple(): + f = StringIO("""\ +[foo] +bar = baz +""") + cfg = conf.ceph.parse(f) + assert cfg.get('foo', 'bar') == 'baz' + + +def test_indent_space(): + f = StringIO("""\ +[foo] + bar = baz +""") + cfg = conf.ceph.parse(f) + assert cfg.get('foo', 'bar') == 'baz' + + +def test_indent_tab(): + f = StringIO("""\ +[foo] +\tbar = baz +""") + cfg = conf.ceph.parse(f) + assert cfg.get('foo', 'bar') == 'baz' + + +def test_words_underscore(): + f = StringIO("""\ +[foo] +bar_thud = baz +""") + cfg = conf.ceph.parse(f) + assert cfg.get('foo', 'bar_thud') == 'baz' + assert cfg.get('foo', 'bar thud') == 'baz' + + +def test_words_space(): + f = StringIO("""\ +[foo] +bar thud = baz +""") + cfg = conf.ceph.parse(f) + assert cfg.get('foo', 'bar_thud') == 'baz' + assert cfg.get('foo', 'bar thud') == 'baz' + + +def test_words_many(): + f = StringIO("""\ +[foo] +bar__ thud quux = baz +""") + cfg = conf.ceph.parse(f) + assert cfg.get('foo', 'bar_thud_quux') == 'baz' + assert cfg.get('foo', 'bar thud quux') == 'baz' + + +def test_write_words_underscore(): + cfg = conf.ceph.CephConf() + cfg.add_section('foo') + cfg.set('foo', 'bar thud quux', 'baz') + f = StringIO() + cfg.write(f) + f.seek(0) + assert f.readlines() == ['[foo]\n', 'bar_thud_quux = baz\n','\n'] + + +def test_section_repeat(): + f = StringIO("""\ +[foo] +bar = bez +thud = quux + +[foo] +bar = baz +""") + cfg = conf.ceph.parse(f) + assert cfg.get('foo', 'bar') == 'baz' + assert cfg.get('foo', 'thud') == 'quux' diff --git a/ceph_deploy/tests/test_gather_keys.py b/ceph_deploy/tests/test_gather_keys.py new file mode 100644 index 0000000..37f5b1c --- /dev/null +++ b/ceph_deploy/tests/test_gather_keys.py @@ -0,0 +1,141 @@ +from ceph_deploy import gatherkeys +from ceph_deploy import new +import mock +import pytest +import tempfile +import os +import shutil + + +def get_key_static(keytype, key_path): + with open(key_path, 'w') as f: + f.write("[%s]\n" % (gatherkeys.keytype_identity(keytype))) + f.write("key=fred\n") + + +def get_key_dynamic(keytype, key_path): + with open(key_path, 'w', 0o600) as f: + f.write("[%s]\n" % (gatherkeys.keytype_identity(keytype))) + f.write("key='%s'" % (new.generate_auth_key())) + + +def mock_time_strftime(time_format): + return "20160412144231" + + +def mock_get_keys_fail(args, host, dest_dir): + return False + + +def mock_get_keys_sucess_static(args, host, dest_dir): + for keytype in ["admin", "mon", "osd", "mds", "mgr", "rgw"]: + keypath = gatherkeys.keytype_path_to(args, keytype) + path = "%s/%s" % (dest_dir, keypath) + get_key_static(keytype, path) + return True + + +def mock_get_keys_sucess_dynamic(args, host, dest_dir): + for keytype in ["admin", "mon", "osd", "mds", "mgr", "rgw"]: + keypath = gatherkeys.keytype_path_to(args, keytype) + path = "%s/%s" % (dest_dir, keypath) + get_key_dynamic(keytype, path) + return True + + +class TestGatherKeys(object): + """ + Since we are testing things that effect the content of the current working + directory we should test in a clean empty directory. + """ + def setup(self): + """ + Make temp directory for tests and set as current working directory + """ + self.orginaldir = os.getcwd() + self.test_dir = tempfile.mkdtemp() + os.chdir(self.test_dir) + + + def teardown(self): + """ + Set current working directory to old value + Remove temp directory and content + """ + os.chdir(self.orginaldir) + shutil.rmtree(self.test_dir) + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_with_mon', mock_get_keys_fail) + def test_gatherkeys_fail(self): + """ + Test 'gatherkeys' fails when connecting to mon fails. + """ + args = mock.Mock() + args.cluster = "ceph" + args.mon = ['host1'] + with pytest.raises(RuntimeError): + gatherkeys.gatherkeys(args) + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_with_mon', mock_get_keys_sucess_static) + def test_gatherkeys_success(self): + """ + Test 'gatherkeys' succeeds when getinig keys that are always the same. + Test 'gatherkeys' does not backup identical keys + """ + args = mock.Mock() + args.cluster = "ceph" + args.mon = ['host1'] + gatherkeys.gatherkeys(args) + dir_content = os.listdir(self.test_dir) + assert "ceph.client.admin.keyring" in dir_content + assert "ceph.bootstrap-mds.keyring" in dir_content + assert "ceph.bootstrap-mgr.keyring" in dir_content + assert "ceph.mon.keyring" in dir_content + assert "ceph.bootstrap-osd.keyring" in dir_content + assert "ceph.bootstrap-rgw.keyring" in dir_content + assert len(dir_content) == 6 + # Now we repeat as no new keys are generated + gatherkeys.gatherkeys(args) + dir_content = os.listdir(self.test_dir) + assert len(dir_content) == 6 + + + @mock.patch('ceph_deploy.gatherkeys.time.strftime', mock_time_strftime) + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_with_mon', mock_get_keys_sucess_dynamic) + def test_gatherkeys_backs_up(self): + """ + Test 'gatherkeys' succeeds when getting keys that are always different. + Test 'gatherkeys' does backup keys that are not identical. + """ + args = mock.Mock() + args.cluster = "ceph" + args.mon = ['host1'] + gatherkeys.gatherkeys(args) + dir_content = os.listdir(self.test_dir) + assert "ceph.client.admin.keyring" in dir_content + assert "ceph.bootstrap-mds.keyring" in dir_content + assert "ceph.bootstrap-mgr.keyring" in dir_content + assert "ceph.mon.keyring" in dir_content + assert "ceph.bootstrap-osd.keyring" in dir_content + assert "ceph.bootstrap-rgw.keyring" in dir_content + assert len(dir_content) == 6 + # Now we repeat as new keys are generated and old + # are backed up + gatherkeys.gatherkeys(args) + dir_content = os.listdir(self.test_dir) + mocked_time = mock_time_strftime(None) + assert "ceph.client.admin.keyring" in dir_content + assert "ceph.bootstrap-mds.keyring" in dir_content + assert "ceph.bootstrap-mgr.keyring" in dir_content + assert "ceph.mon.keyring" in dir_content + assert "ceph.bootstrap-osd.keyring" in dir_content + assert "ceph.bootstrap-rgw.keyring" in dir_content + assert "ceph.client.admin.keyring-%s" % (mocked_time) in dir_content + assert "ceph.bootstrap-mds.keyring-%s" % (mocked_time) in dir_content + assert "ceph.bootstrap-mgr.keyring-%s" % (mocked_time) in dir_content + assert "ceph.mon.keyring-%s" % (mocked_time) in dir_content + assert "ceph.bootstrap-osd.keyring-%s" % (mocked_time) in dir_content + assert "ceph.bootstrap-rgw.keyring-%s" % (mocked_time) in dir_content + assert len(dir_content) == 12 diff --git a/ceph_deploy/tests/test_gather_keys_missing.py b/ceph_deploy/tests/test_gather_keys_missing.py new file mode 100644 index 0000000..36b2654 --- /dev/null +++ b/ceph_deploy/tests/test_gather_keys_missing.py @@ -0,0 +1,179 @@ +from ceph_deploy import gatherkeys +from ceph_deploy import new +import mock +import tempfile +import shutil +import os +import pytest + + +class mock_conn(object): + def __init__(self): + pass + +class mock_distro(object): + def __init__(self): + self.conn = mock_conn() + +class mock_rlogger(object): + def error(self, *arg): + return + + def debug(self, *arg): + return + + +def mock_remoto_process_check_success(conn, args): + secret = new.generate_auth_key() + out = '[mon.]\nkey = %s\ncaps mon = allow *\n' % secret + return out.split('\n'), [], 0 + + +def mock_remoto_process_check_rc_error(conn, args): + return [""], ["this failed\n"], 1 + + +class TestGatherKeysMissing(object): + """ + Since we are testing things that effect the content a directory we should + test in a clean empty directory. + """ + + def setup(self): + """ + Make temp directory for tests. + """ + self.args = mock.Mock() + self.distro = mock_distro() + self.test_dir = tempfile.mkdtemp() + self.rlogger = mock_rlogger() + self.keypath_remote = "some_path" + + def teardown(self): + """ + Remove temp directory and content + """ + shutil.rmtree(self.test_dir) + + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + def test_success_admin(self): + keytype = 'admin' + rc = gatherkeys.gatherkeys_missing( + self.args, + self.distro, + self.rlogger, + self.keypath_remote, + keytype, + self.test_dir + ) + assert rc is True + keyname = gatherkeys.keytype_path_to(self.args, keytype) + keypath_gen = os.path.join(self.test_dir, keyname) + assert os.path.isfile(keypath_gen) + + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + def test_success_mds(self): + keytype = 'mds' + rc = gatherkeys.gatherkeys_missing( + self.args, + self.distro, + self.rlogger, + self.keypath_remote, + keytype, + self.test_dir + ) + assert rc is True + keyname = gatherkeys.keytype_path_to(self.args, keytype) + keypath_gen = os.path.join(self.test_dir, keyname) + assert os.path.isfile(keypath_gen) + + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + def test_success_mgr(self): + keytype = 'mgr' + rc = gatherkeys.gatherkeys_missing( + self.args, + self.distro, + self.rlogger, + self.keypath_remote, + keytype, + self.test_dir + ) + assert rc is True + keyname = gatherkeys.keytype_path_to(self.args, keytype) + keypath_gen = os.path.join(self.test_dir, keyname) + assert os.path.isfile(keypath_gen) + + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + def test_success_osd(self): + keytype = 'osd' + rc = gatherkeys.gatherkeys_missing( + self.args, + self.distro, + self.rlogger, + self.keypath_remote, + keytype, + self.test_dir + ) + assert rc is True + keyname = gatherkeys.keytype_path_to(self.args, keytype) + keypath_gen = os.path.join(self.test_dir, keyname) + assert os.path.isfile(keypath_gen) + + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + def test_success_rgw(self): + keytype = 'rgw' + rc = gatherkeys.gatherkeys_missing( + self.args, + self.distro, + self.rlogger, + self.keypath_remote, + keytype, + self.test_dir + ) + assert rc is True + keyname = gatherkeys.keytype_path_to(self.args, keytype) + keypath_gen = os.path.join(self.test_dir, keyname) + assert os.path.isfile(keypath_gen) + + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_rc_error) + def test_remoto_process_check_rc_error(self): + keytype = 'admin' + rc = gatherkeys.gatherkeys_missing( + self.args, + self.distro, + self.rlogger, + self.keypath_remote, + keytype, + self.test_dir + ) + assert rc is False + keyname = gatherkeys.keytype_path_to(self.args, keytype) + keypath_gen = os.path.join(self.test_dir, keyname) + assert not os.path.isfile(keypath_gen) + + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + def test_fail_identity_missing(self): + keytype = 'silly' + with pytest.raises(RuntimeError): + gatherkeys.gatherkeys_missing( + self.args, + self.distro, + self.rlogger, + self.keypath_remote, + keytype, + self.test_dir + ) + + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + def test_fail_capabilities_missing(self): + keytype = 'mon' + with pytest.raises(RuntimeError): + gatherkeys.gatherkeys_missing( + self.args, + self.distro, + self.rlogger, + self.keypath_remote, + keytype, + self.test_dir + ) + diff --git a/ceph_deploy/tests/test_gather_keys_with_mon.py b/ceph_deploy/tests/test_gather_keys_with_mon.py new file mode 100644 index 0000000..e7d1ada --- /dev/null +++ b/ceph_deploy/tests/test_gather_keys_with_mon.py @@ -0,0 +1,219 @@ +from ceph_deploy import gatherkeys +from ceph_deploy import new +import mock +import json +import copy + + +remoto_process_check_success_output = { + "name": "ceph-node1", + "rank": 0, + "state": "leader", + "election_epoch": 6, + "quorum": [ + 0, + 1, + 2 + ], + "outside_quorum": [], + "extra_probe_peers": [ + "192.168.99.125:6789\/0", + "192.168.99.126:6789\/0" + ], + "sync_provider": [], + "monmap": { + "epoch": 1, + "fsid": "4dbee7eb-929b-4f3f-ad23-8a4e47235e40", + "modified": "2016-04-11 05:35:21.665220", + "created": "2016-04-11 05:35:21.665220", + "mons": [ + { + "rank": 0, + "name": "host0", + "addr": "192.168.99.124:6789\/0" + }, + { + "rank": 1, + "name": "host1", + "addr": "192.168.99.125:6789\/0" + }, + { + "rank": 2, + "name": "host2", + "addr": "192.168.99.126:6789\/0" + } + ] + } + } + + +class mock_remote_module(object): + def get_file(self, path): + return self.get_file_result + + def shortname(self): + hostname_split = self.longhostname.split('.') + return hostname_split[0] + +class mock_conn(object): + def __init__(self): + self.remote_module = mock_remote_module() + + +class mock_distro(object): + def __init__(self): + self.conn = mock_conn() + + +def mock_hosts_get_file_key_content(host, **kwargs): + output = mock_distro() + mon_keyring = '[mon.]\nkey = %s\ncaps mon = allow *\n' % new.generate_auth_key() + output.conn.remote_module.get_file_result = mon_keyring + output.conn.remote_module.longhostname = host + return output + + +def mock_hosts_get_file_key_content_none(host, **kwargs): + output = mock_distro() + output.conn.remote_module.get_file_result = None + output.conn.remote_module.longhostname = host + return output + + +def mock_gatherkeys_missing_success(args, distro, rlogger, path_keytype_mon, keytype, dest_dir): + return True + + +def mock_gatherkeys_missing_fail(args, distro, rlogger, path_keytype_mon, keytype, dest_dir): + return False + + +def mock_remoto_process_check_success(conn, args): + out = json.dumps(remoto_process_check_success_output,sort_keys=True, indent=4) + return out.split('\n'), [], 0 + + +def mock_remoto_process_check_rc_error(conn, args): + return [""], ["this failed\n"], 1 + + +def mock_remoto_process_check_out_not_json(conn, args): + return ["}bad output{"], [""], 0 + + +def mock_remoto_process_check_out_missing_quorum(conn, args): + outdata = copy.deepcopy(remoto_process_check_success_output) + del outdata["quorum"] + out = json.dumps(outdata,sort_keys=True, indent=4) + return out.split('\n'), [], 0 + + +def mock_remoto_process_check_out_missing_quorum_1(conn, args): + outdata = copy.deepcopy(remoto_process_check_success_output) + del outdata["quorum"][1] + out = json.dumps(outdata,sort_keys=True, indent=4) + return out.split('\n'), [], 0 + + +def mock_remoto_process_check_out_missing_monmap(conn, args): + outdata = copy.deepcopy(remoto_process_check_success_output) + del outdata["monmap"] + out = json.dumps(outdata,sort_keys=True, indent=4) + return out.split('\n'), [], 0 + + +def mock_remoto_process_check_out_missing_mons(conn, args): + outdata = copy.deepcopy(remoto_process_check_success_output) + del outdata["monmap"]["mons"] + out = json.dumps(outdata,sort_keys=True, indent=4) + return out.split('\n'), [], 0 + + +def mock_remoto_process_check_out_missing_monmap_host1(conn, args): + outdata = copy.deepcopy(remoto_process_check_success_output) + del outdata["monmap"]["mons"][1] + out = json.dumps(outdata,sort_keys=True, indent=4) + return out.split('\n'), [], 0 + + +class TestGatherKeysWithMon(object): + """ + Test gatherkeys_with_mon function + """ + def setup(self): + self.args = mock.Mock() + self.args.cluster = "ceph" + self.args.mon = ['host1'] + self.host = 'host1' + self.test_dir = '/tmp' + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_success) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content) + def test_success(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is True + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_success) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content_none) + def test_monkey_none(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is False + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_fail) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_success) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content) + def test_missing_fail(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is False + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_success) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_rc_error) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content) + def test_remoto_process_check_rc_error(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is False + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_success) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_out_not_json) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content) + def test_remoto_process_check_out_not_json(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is False + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_success) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_out_missing_quorum) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content) + def test_remoto_process_check_out_missing_quorum(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is False + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_success) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_out_missing_quorum_1) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content) + def test_remoto_process_check_out_missing_quorum_1(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is False + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_success) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_out_missing_mons) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content) + def test_remoto_process_check_out_missing_mon(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is False + + + @mock.patch('ceph_deploy.gatherkeys.gatherkeys_missing', mock_gatherkeys_missing_success) + @mock.patch('ceph_deploy.lib.remoto.process.check', mock_remoto_process_check_out_missing_monmap_host1) + @mock.patch('ceph_deploy.hosts.get', mock_hosts_get_file_key_content) + def test_remoto_process_check_out_missing_monmap_host1(self): + rc = gatherkeys.gatherkeys_with_mon(self.args, self.host, self.test_dir) + assert rc is False diff --git a/ceph_deploy/tests/test_install.py b/ceph_deploy/tests/test_install.py new file mode 100644 index 0000000..4a47f9d --- /dev/null +++ b/ceph_deploy/tests/test_install.py @@ -0,0 +1,149 @@ +from mock import Mock + +from ceph_deploy import install + + +class TestSanitizeArgs(object): + + def setup(self): + self.args = Mock() + # set the default behavior we set in cli.py + self.args.default_release = False + self.args.stable = None + + def test_args_release_not_specified(self): + self.args.release = None + result = install.sanitize_args(self.args) + # XXX + # we should get `args.release` to be the latest release + # but we don't want to be updating this test every single + # time there is a new default value, and we can't programatically + # change that. Future improvement: make the default release a + # variable in `ceph_deploy/__init__.py` + assert result.default_release is True + + def test_args_release_is_specified(self): + self.args.release = 'dumpling' + result = install.sanitize_args(self.args) + assert result.default_release is False + + def test_args_release_stable_is_used(self): + self.args.stable = 'dumpling' + result = install.sanitize_args(self.args) + assert result.release == 'dumpling' + + def test_args_stable_is_not_used(self): + self.args.release = 'dumpling' + result = install.sanitize_args(self.args) + assert result.stable is None + + +class TestDetectComponents(object): + + def setup(self): + self.args = Mock() + # default values for install_* flags + self.args.install_all = False + self.args.install_mds = False + self.args.install_mgr = False + self.args.install_mon = False + self.args.install_osd = False + self.args.install_rgw = False + self.args.install_tests = False + self.args.install_common = False + self.args.repo = False + self.distro = Mock() + + def test_install_with_repo_option_returns_no_packages(self): + self.args.repo = True + result = install.detect_components(self.args, self.distro) + assert result == [] + + def test_install_all_returns_all_packages_deb(self): + self.args.install_all = True + self.distro.is_rpm = False + self.distro.is_deb = True + self.distro.is_pkgtarxz = False + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph-osd', 'ceph-mds', 'ceph', 'ceph-mon', 'radosgw' + ]) + + def test_install_all_with_other_options_returns_all_packages_deb(self): + self.distro.is_rpm = False + self.distro.is_deb = True + self.distro.is_pkgtarxz = False + self.args.install_all = True + self.args.install_mds = True + self.args.install_mgr = True + self.args.install_mon = True + self.args.install_osd = True + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph-osd', 'ceph-mds', 'ceph', 'ceph-mon', 'radosgw' + ]) + + def test_install_all_returns_all_packages_rpm(self): + self.args.install_all = True + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph-osd', 'ceph-mds', 'ceph', 'ceph-mon', 'ceph-radosgw' + ]) + + def test_install_all_with_other_options_returns_all_packages_rpm(self): + self.args.install_all = True + self.args.install_mds = True + self.args.install_mon = True + self.args.install_mgr = True + self.args.install_osd = True + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph-osd', 'ceph-mds', 'ceph', 'ceph-mon', 'ceph-radosgw' + ]) + + def test_install_all_returns_all_packages_pkgtarxz(self): + self.args.install_all = True + self.distro.is_rpm = False + self.distro.is_deb = False + self.distro.is_pkgtarxz = True + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph', + ]) + + def test_install_all_with_other_options_returns_all_packages_pkgtarxz(self): + self.distro.is_rpm = False + self.distro.is_deb = False + self.distro.is_pkgtarxz = True + self.args.install_all = True + self.args.install_mds = True + self.args.install_mgr = True + self.args.install_mon = True + self.args.install_osd = True + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph', + ]) + + def test_install_only_one_component(self): + self.args.install_osd = True + result = install.detect_components(self.args, self.distro) + assert result == ['ceph-osd'] + + def test_install_a_couple_of_components(self): + self.args.install_osd = True + self.args.install_mds = True + self.args.install_mgr = True + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted(['ceph-osd', 'ceph-mds', 'ceph-mgr']) + + def test_install_tests(self): + self.args.install_tests = True + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted(['ceph-test']) + + def test_install_all_should_be_default_when_no_options_passed(self): + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph-osd', 'ceph-mds', 'ceph', 'ceph-mon', 'ceph-radosgw' + ]) diff --git a/ceph_deploy/tests/test_keys_equivalent.py b/ceph_deploy/tests/test_keys_equivalent.py new file mode 100644 index 0000000..c748baf --- /dev/null +++ b/ceph_deploy/tests/test_keys_equivalent.py @@ -0,0 +1,171 @@ +from ceph_deploy import gatherkeys +from ceph_deploy import new +import tempfile +import shutil +import pytest + + +def write_key_mon_with_caps(path, secret): + mon_keyring = '[mon.]\nkey = %s\ncaps mon = allow *\n' % secret + with open(path, 'w', 0o600) as f: + f.write(mon_keyring) + + +def write_key_mon_with_caps_with_tab(path, secret): + mon_keyring = '[mon.]\n\tkey = %s\n\tcaps mon = allow *\n' % secret + with open(path, 'w', 0o600) as f: + f.write(mon_keyring) + + +def write_key_mon_with_caps_with_tab_quote(path, secret): + mon_keyring = '[mon.]\n\tkey = %s\n\tcaps mon = "allow *"\n' % secret + with open(path, 'w', 0o600) as f: + f.write(mon_keyring) + + +def write_key_mon_without_caps(path, secret): + mon_keyring = '[mon.]\nkey = %s\n' % secret + with open(path, 'w', 0o600) as f: + f.write(mon_keyring) + + +class TestKeysEquivalent(object): + """ + Since we are testing things that effect the content of the current working + directory we should test in a clean empty directory. + """ + def setup(self): + """ + Make temp directory for tests. + """ + self.test_dir = tempfile.mkdtemp() + + + def teardown(self): + """ + Remove temp directory and content + """ + shutil.rmtree(self.test_dir) + + + def test_identical_with_caps(self): + secret_01 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps(key_path_01, secret_01) + write_key_mon_with_caps(key_path_02, secret_01) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is True + + + def test_different_with_caps(self): + secret_01 = new.generate_auth_key() + secret_02 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps(key_path_01, secret_01) + write_key_mon_with_caps(key_path_02, secret_02) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is False + + + def test_identical_without_caps(self): + secret_01 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_without_caps(key_path_01, secret_01) + write_key_mon_without_caps(key_path_02, secret_01) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is True + + + def test_different_without_caps(self): + secret_01 = new.generate_auth_key() + secret_02 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_without_caps(key_path_01, secret_01) + write_key_mon_without_caps(key_path_02, secret_02) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is False + + + def test_identical_mixed_caps(self): + secret_01 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps(key_path_01, secret_01) + write_key_mon_without_caps(key_path_02, secret_01) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is True + + + def test_different_mixed_caps(self): + secret_01 = new.generate_auth_key() + secret_02 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps(key_path_01, secret_01) + write_key_mon_without_caps(key_path_02, secret_02) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is False + + + def test_identical_caps_mixed_tabs(self): + secret_01 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps(key_path_01, secret_01) + write_key_mon_with_caps_with_tab(key_path_02, secret_01) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is True + + + def test_different_caps_mixed_tabs(self): + secret_01 = new.generate_auth_key() + secret_02 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps(key_path_01, secret_01) + write_key_mon_with_caps_with_tab(key_path_02, secret_02) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is False + + + def test_identical_caps_mixed_quote(self): + secret_01 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps_with_tab(key_path_01, secret_01) + write_key_mon_with_caps_with_tab_quote(key_path_02, secret_01) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is True + + + def test_different_caps_mixed_quote(self): + secret_01 = new.generate_auth_key() + secret_02 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps_with_tab(key_path_01, secret_01) + write_key_mon_with_caps_with_tab_quote(key_path_02, secret_02) + same = gatherkeys._keyring_equivalent(key_path_01, key_path_02) + assert same is False + + + def test_missing_key_1(self): + secret_02 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps_with_tab_quote(key_path_02, secret_02) + with pytest.raises(IOError): + gatherkeys._keyring_equivalent(key_path_01, key_path_02) + + + def test_missing_key_2(self): + secret_01 = new.generate_auth_key() + key_path_01 = self.test_dir + "/01.keyring" + key_path_02 = self.test_dir + "/02.keyring" + write_key_mon_with_caps_with_tab_quote(key_path_01, secret_01) + with pytest.raises(IOError): + gatherkeys._keyring_equivalent(key_path_01, key_path_02) diff --git a/ceph_deploy/tests/test_mon.py b/ceph_deploy/tests/test_mon.py new file mode 100644 index 0000000..7e73cad --- /dev/null +++ b/ceph_deploy/tests/test_mon.py @@ -0,0 +1,95 @@ +from ceph_deploy import exc, mon +from ceph_deploy.conf.ceph import CephConf +from mock import Mock +import pytest + + +def make_fake_conf(): + return CephConf() + +# NOTE: If at some point we re-use this helper, move it out +# and make it even more generic + +def make_fake_conn(receive_returns=None): + receive_returns = receive_returns or (['{}'], [], 0) + conn = Mock() + conn.cmd = lambda x: x + conn.sudo = '' + conn.return_value = conn + conn.execute = conn + conn.receive = Mock(return_value=receive_returns) + conn.gateway.remote_exec = conn.receive + conn.result = Mock(return_value=conn) + conn.cmd = lambda x: x + return conn + + +class TestGetMonInitialMembers(object): + + def test_assert_if_mon_none_and_empty_True(self): + cfg = make_fake_conf() + with pytest.raises(exc.NeedHostError): + mon.get_mon_initial_members(Mock(), True, cfg) + + def test_return_if_mon_none_and_empty_false(self): + cfg = make_fake_conf() + mon_initial_members = mon.get_mon_initial_members(Mock(), False, cfg) + assert mon_initial_members is None + + def test_single_item_if_mon_not_none(self): + cfg = make_fake_conf() + cfg.add_section('global') + cfg.set('global', 'mon initial members', 'AAAA') + mon_initial_members = mon.get_mon_initial_members(Mock(), False, cfg) + assert set(mon_initial_members) == set(['AAAA']) + + def test_multiple_item_if_mon_not_none(self): + cfg = make_fake_conf() + cfg.add_section('global') + cfg.set('global', 'mon initial members', 'AAAA, BBBB') + mon_initial_members = mon.get_mon_initial_members(Mock(), False, cfg) + assert set(mon_initial_members) == set(['AAAA', 'BBBB']) + + +class TestCatchCommonErrors(object): + + def setup(self): + self.logger = Mock() + + def assert_logger_message(self, logger, msg): + calls = logger.call_args_list + for log_call in calls: + if msg in log_call[0][0]: + return True + raise AssertionError('"%s" was not found in any of %s' % (msg, calls)) + + def test_warn_if_no_intial_members(self): + fake_conn = make_fake_conn() + cfg = make_fake_conf() + mon.catch_mon_errors(fake_conn, self.logger, 'host', cfg, Mock()) + expected_msg = 'is not defined in `mon initial members`' + self.assert_logger_message(self.logger.warning, expected_msg) + + def test_warn_if_host_not_in_intial_members(self): + fake_conn = make_fake_conn() + cfg = make_fake_conf() + cfg.add_section('global') + cfg.set('global', 'mon initial members', 'AAAA') + mon.catch_mon_errors(fake_conn, self.logger, 'host', cfg, Mock()) + expected_msg = 'is not defined in `mon initial members`' + self.assert_logger_message(self.logger.warning, expected_msg) + + def test_warn_if_not_mon_in_monmap(self): + fake_conn = make_fake_conn() + cfg = make_fake_conf() + mon.catch_mon_errors(fake_conn, self.logger, 'host', cfg, Mock()) + expected_msg = 'does not exist in monmap' + self.assert_logger_message(self.logger.warning, expected_msg) + + def test_warn_if_not_public_addr_and_not_public_netw(self): + fake_conn = make_fake_conn() + cfg = make_fake_conf() + cfg.add_section('global') + mon.catch_mon_errors(fake_conn, self.logger, 'host', cfg, Mock()) + expected_msg = 'neither `public_addr` nor `public_network`' + self.assert_logger_message(self.logger.warning, expected_msg) diff --git a/ceph_deploy/tests/test_remotes.py b/ceph_deploy/tests/test_remotes.py new file mode 100644 index 0000000..4c7ac03 --- /dev/null +++ b/ceph_deploy/tests/test_remotes.py @@ -0,0 +1,255 @@ +from mock import patch +from ceph_deploy.hosts import remotes +from ceph_deploy.hosts.remotes import platform_information, parse_os_release + +class FakeExists(object): + + def __init__(self, existing_paths): + self.existing_paths = existing_paths + + def __call__(self, path): + for existing_path in self.existing_paths: + if path == existing_path: + return path + + +class TestWhich(object): + + def setup(self): + self.exists_module = 'ceph_deploy.hosts.remotes.os.path.exists' + + def test_finds_absolute_paths(self): + exists = FakeExists(['/bin/ls']) + with patch(self.exists_module, exists): + path = remotes.which('ls') + assert path == '/bin/ls' + + def test_does_not_find_executable(self): + exists = FakeExists(['/bin/foo']) + with patch(self.exists_module, exists): + path = remotes.which('ls') + assert path is None + +class TestPlatformInformation(object): + """ tests various inputs that remotes.platform_information handles + + you can test your OS string by comparing the results with the output from: + python -c "import platform; print platform.linux_distribution()" + """ + + def setup(self): + pass + + def test_handles_deb_version_num(self): + def fake_distro(): return ('debian', '8.4', '') + distro, release, codename = platform_information(fake_distro) + assert distro == 'debian' + assert release == '8.4' + assert codename == 'jessie' + + def test_handles_deb_version_slash(self): + def fake_distro(): return ('debian', 'wheezy/something', '') + distro, release, codename = platform_information(fake_distro) + assert distro == 'debian' + assert release == 'wheezy/something' + assert codename == 'wheezy' + + def test_handles_deb_version_slash_sid(self): + def fake_distro(): return ('debian', 'jessie/sid', '') + distro, release, codename = platform_information(fake_distro) + assert distro == 'debian' + assert release == 'jessie/sid' + assert codename == 'sid' + + def test_handles_no_codename(self): + def fake_distro(): return ('SlaOS', '99.999', '') + distro, release, codename = platform_information(fake_distro) + assert distro == 'SlaOS' + assert release == '99.999' + assert codename == '' + + # Normal distro strings + def test_hanles_centos_64(self): + def fake_distro(): return ('CentOS', '6.4', 'Final') + distro, release, codename = platform_information(fake_distro) + assert distro == 'CentOS' + assert release == '6.4' + assert codename == 'Final' + + + def test_handles_ubuntu_percise(self): + def fake_distro(): return ('Ubuntu', '12.04', 'precise') + distro, release, codename = platform_information(fake_distro) + assert distro == 'Ubuntu' + assert release == '12.04' + assert codename == 'precise' + +class TestParseOsRelease(object): + """ test various forms of /etc/os-release """ + + def setup(self): + pass + + def test_handles_centos_7(self, tmpdir): + path = str(tmpdir.join('os_release')) + with open(path, 'w') as os_release: + os_release.write(""" +NAME="CentOS Linux" +VERSION="7 (Core)" +ID="centos" +ID_LIKE="rhel fedora" +VERSION_ID="7" +PRETTY_NAME="CentOS Linux 7 (Core)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:centos:centos:7" +HOME_URL="https://www.centos.org/" +BUG_REPORT_URL="https://bugs.centos.org/" + +CENTOS_MANTISBT_PROJECT="CentOS-7" +CENTOS_MANTISBT_PROJECT_VERSION="7" +REDHAT_SUPPORT_PRODUCT="centos" +REDHAT_SUPPORT_PRODUCT_VERSION="7" +""") + distro, release, codename = parse_os_release(path) + assert distro == 'centos' + assert release == '7' + assert codename == 'core' + + + def test_handles_debian_stretch(self, tmpdir): + path = str(tmpdir.join('os_release')) + with open(path, 'w') as os_release: + os_release.write(""" +PRETTY_NAME="Debian GNU/Linux 9 (stretch)" +NAME="Debian GNU/Linux" +VERSION_ID="9" +VERSION="9 (stretch)" +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" +""") + distro, release, codename = parse_os_release(path) + assert distro == 'debian' + assert release == '9' + assert codename == 'stretch' + + def test_handles_fedora_26(self, tmpdir): + path = str(tmpdir.join('os_release')) + with open(path, 'w') as os_release: + os_release.write(""" +NAME=Fedora +VERSION="26 (Twenty Six)" +ID=fedora +VERSION_ID=26 +PRETTY_NAME="Fedora 26 (Twenty Six)" +ANSI_COLOR="0;34" +CPE_NAME="cpe:/o:fedoraproject:fedora:26" +HOME_URL="https://fedoraproject.org/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" +REDHAT_BUGZILLA_PRODUCT="Fedora" +REDHAT_BUGZILLA_PRODUCT_VERSION=26 +REDHAT_SUPPORT_PRODUCT="Fedora" +REDHAT_SUPPORT_PRODUCT_VERSION=26 +PRIVACY_POLICY_URL=https://fedoraproject.org/wiki/Legal:PrivacyPolicy +""") + distro, release, codename = parse_os_release(path) + assert distro == 'fedora' + assert release == '26' + assert codename == 'twenty six' + + def test_handles_opensuse_leap_42_2(self, tmpdir): + path = str(tmpdir.join('os_release')) + with open(path, 'w') as os_release: + os_release.write(""" +NAME="openSUSE Leap" +VERSION="42.2" +ID=opensuse +ID_LIKE="suse" +VERSION_ID="42.2" +PRETTY_NAME="openSUSE Leap 42.2" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:opensuse:leap:42.2" +BUG_REPORT_URL="https://bugs.opensuse.org" +HOME_URL="https://www.opensuse.org/" +""") + distro, release, codename = parse_os_release(path) + assert distro == 'opensuse' + assert release == '42.2' + assert codename == '42.2' + + def test_handles_opensuse_tumbleweed(self, tmpdir): + path = str(tmpdir.join('os_release')) + with open(path, 'w') as os_release: + os_release.write(""" +NAME="openSUSE Tumbleweed" +# VERSION="20170502" +ID=opensuse +ID_LIKE="suse" +VERSION_ID="20170502" +PRETTY_NAME="openSUSE Tumbleweed" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:opensuse:tumbleweed:20170502" +BUG_REPORT_URL="https://bugs.opensuse.org" +HOME_URL="https://www.opensuse.org/" +""") + distro, release, codename = parse_os_release(path) + assert distro == 'opensuse' + assert release == '20170502' + assert codename == 'tumbleweed' + + def test_handles_sles_12_sp3(self, tmpdir): + path = str(tmpdir.join('os_release')) + with open(path, 'w') as os_release: + os_release.write(""" +NAME="SLES" +VERSION="12-SP3" +VERSION_ID="12.3" +PRETTY_NAME="SUSE Linux Enterprise Server 12 SP3" +ID="sles" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:suse:sles:12:sp3" +""") + distro, release, codename = parse_os_release(path) + assert distro == 'sles' + assert release == '12.3' + assert codename == '12-SP3' + + def test_handles_ubuntu_xenial(self, tmpdir): + path = str(tmpdir.join('os_release')) + with open(path, 'w') as os_release: + os_release.write(""" +NAME="Ubuntu" +VERSION="16.04 LTS (Xenial Xerus)" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 16.04 LTS" +VERSION_ID="16.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" +UBUNTU_CODENAME=xenial +""") + distro, release, codename = parse_os_release(path) + assert distro == 'ubuntu' + assert release == '16.04' + assert codename == 'xenial' + + def test_handles_alt_8_2(self, tmpdir): + path = str(tmpdir.join('os_release')) + with open(path, 'w') as os_release: + os_release.write(""" +NAME="ALT" +VERSION="8.2 " +ID=altlinux +VERSION_ID=8.2 +PRETTY_NAME="ALT Workstation K 8.2 (Centaurea Ruthenica)" +ANSI_COLOR="1;33" +CPE_NAME="cpe:/o:alt:kworkstation:8.2" +HOME_URL="http://www.basealt.ru" +BUG_REPORT_URL="https://bugs.altlinux.org/" +""") + distro, release, codename = parse_os_release(path) + assert distro == 'altlinux' + assert release == '8.2' + assert codename == '8.2' diff --git a/ceph_deploy/tests/unit/hosts/test_altlinux.py b/ceph_deploy/tests/unit/hosts/test_altlinux.py new file mode 100644 index 0000000..cc65fde --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_altlinux.py @@ -0,0 +1,10 @@ +from ceph_deploy.hosts.alt.install import map_components, NON_SPLIT_PACKAGES + + +class TestALTMapComponents(object): + def test_valid(self): + pkgs = map_components(NON_SPLIT_PACKAGES, ['ceph-osd', 'ceph-common', 'ceph-radosgw']) + assert 'ceph' in pkgs + assert 'ceph-common' in pkgs + assert 'ceph-radosgw' in pkgs + assert 'ceph-osd' not in pkgs diff --git a/ceph_deploy/tests/unit/hosts/test_centos.py b/ceph_deploy/tests/unit/hosts/test_centos.py new file mode 100644 index 0000000..187582c --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_centos.py @@ -0,0 +1,64 @@ +from ceph_deploy.hosts import centos +from ceph_deploy import hosts +from mock import Mock, patch + + +def pytest_generate_tests(metafunc): + # called once per each test function + try: + funcarglist = metafunc.cls.params[metafunc.function.__name__] + except AttributeError: + return + argnames = list(funcarglist[0]) + metafunc.parametrize(argnames, [[funcargs[name] for name in argnames] + for funcargs in funcarglist]) + + +class TestCentosRepositoryUrlPart(object): + + params= { + 'test_repository_url_part': [ + dict(distro="CentOS Linux", release='4.3', codename="Foo", output='el6'), + dict(distro="CentOS Linux", release='6.5', codename="Final", output='el6'), + dict(distro="CentOS Linux", release='7.0', codename="Core", output='el7'), + dict(distro="CentOS Linux", release='7.0.1406', codename="Core", output='el7'), + dict(distro="CentOS Linux", release='10.4.000', codename="Core", output='el10'), + dict(distro="RedHat", release='4.3', codename="Foo", output='el6'), + dict(distro="Red Hat Enterprise Linux Server", release='5.8', codename="Tikanga", output="el6"), + dict(distro="Red Hat Enterprise Linux Server", release='6.5', codename="Santiago", output='rhel6'), + dict(distro="RedHat", release='7.0.1406', codename="Core", output='rhel7'), + dict(distro="RedHat", release='10.999.12', codename="Core", output='rhel10'), + ], + 'test_rpm_dist': [ + dict(distro="CentOS Linux", release='4.3', codename="Foo", output='el6'), + dict(distro="CentOS Linux", release='6.5', codename="Final", output='el6'), + dict(distro="CentOS Linux", release='7.0', codename="Core", output='el7'), + dict(distro="CentOS Linux", release='7.0.1406', codename="Core", output='el7'), + dict(distro="CentOS Linux", release='10.10.9191', codename="Core", output='el10'), + dict(distro="RedHat", release='4.3', codename="Foo", output='el6'), + dict(distro="Red Hat Enterprise Linux Server", release='5.8', codename="Tikanga", output="el6"), + dict(distro="Red Hat Enterprise Linux Server", release='6.5', codename="Santiago", output='el6'), + dict(distro="RedHat", release='7.0', codename="Core", output='el7'), + dict(distro="RedHat", release='7.0.1406', codename="Core", output='el7'), + dict(distro="RedHat", release='10.9.8765', codename="Core", output='el10'), + ] + } + + def make_fake_connection(self, platform_information=None): + get_connection = Mock() + get_connection.return_value = get_connection + get_connection.remote_module.platform_information = Mock( + return_value=platform_information) + return get_connection + + def test_repository_url_part(self, distro, release, codename, output): + fake_get_connection = self.make_fake_connection((distro, release, codename)) + with patch('ceph_deploy.hosts.get_connection', fake_get_connection): + self.module = hosts.get('testhost') + assert centos.repository_url_part(self.module) == output + + def test_rpm_dist(self, distro, release, codename, output): + fake_get_connection = self.make_fake_connection((distro, release, codename)) + with patch('ceph_deploy.hosts.get_connection', fake_get_connection): + self.module = hosts.get('testhost') + assert centos.rpm_dist(self.module) == output diff --git a/ceph_deploy/tests/unit/hosts/test_common.py b/ceph_deploy/tests/unit/hosts/test_common.py new file mode 100644 index 0000000..278d09e --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_common.py @@ -0,0 +1,24 @@ +from ceph_deploy.hosts.common import map_components + + +class TestMapComponents(object): + + def test_map_components_all_split(self): + components = ['ceph-mon', 'ceph-osd'] + packages = map_components([], components) + assert set(packages) == set(components) + + def test_map_components_mds_not_split(self): + components = ['ceph-mon', 'ceph-osd', 'ceph-mds'] + packages = map_components(['ceph-mds'], components) + assert set(packages) == set(['ceph-mon', 'ceph-osd', 'ceph']) + + def test_map_components_no_duplicates(self): + components = ['ceph-mon', 'ceph-osd', 'ceph-mds'] + packages = map_components(['ceph-mds', 'ceph-osd'], components) + assert set(packages) == set(['ceph-mon', 'ceph']) + assert len(packages) == len(set(['ceph-mon', 'ceph'])) + + def test_map_components_no_components(self): + packages = map_components(['ceph-mon'], []) + assert len(packages) == 0 diff --git a/ceph_deploy/tests/unit/hosts/test_hosts.py b/ceph_deploy/tests/unit/hosts/test_hosts.py new file mode 100644 index 0000000..0e60be0 --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_hosts.py @@ -0,0 +1,437 @@ +from pytest import raises +from mock import Mock, patch + +from ceph_deploy import exc +from ceph_deploy import hosts + + +class TestNormalized(object): + + def test_get_debian(self): + result = hosts._normalized_distro_name('Debian') + assert result == 'debian' + + def test_get_centos(self): + result = hosts._normalized_distro_name('CentOS Linux') + assert result == 'centos' + + def test_get_ubuntu(self): + result = hosts._normalized_distro_name('Ubuntu') + assert result == 'ubuntu' + + def test_get_mint(self): + result = hosts._normalized_distro_name('LinuxMint') + assert result == 'ubuntu' + + def test_get_suse(self): + result = hosts._normalized_distro_name('SUSE LINUX') + assert result == 'suse' + + def test_get_redhat(self): + result = hosts._normalized_distro_name('RedHatEnterpriseLinux') + assert result == 'redhat' + + def test_get_virtuozzo(self): + result = hosts._normalized_distro_name('Virtuozzo Linux') + assert result == 'virtuozzo' + + def test_get_arch(self): + result = hosts._normalized_distro_name('Arch Linux') + assert result == 'arch' + + +class TestNormalizeRelease(object): + + def test_int_single_version(self): + result = hosts._normalized_release('1') + assert result.int_major == 1 + assert result.int_minor == 0 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_single_version_with_trailing_space(self): + result = hosts._normalized_release(' 1') + assert result.int_major == 1 + assert result.int_minor == 0 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_single_version_with_prepended_zero(self): + result = hosts._normalized_release('01') + assert result.int_major == 1 + assert result.int_minor == 0 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_minor_version(self): + result = hosts._normalized_release('1.8') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_minor_version_with_trailing_space(self): + result = hosts._normalized_release(' 1.8') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_minor_version_with_prepended_zero(self): + result = hosts._normalized_release('01.08') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_patch_version(self): + result = hosts._normalized_release('1.8.1234') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 0 + + def test_int_patch_version_with_trailing_space(self): + result = hosts._normalized_release(' 1.8.1234') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 0 + + def test_int_patch_version_with_prepended_zero(self): + result = hosts._normalized_release('01.08.01234') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 0 + + def test_int_garbage_version(self): + result = hosts._normalized_release('1.8.1234.1') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 1 + + def test_int_garbage_version_with_trailing_space(self): + result = hosts._normalized_release(' 1.8.1234.1') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 1 + + def test_int_garbage_version_with_prepended_zero(self): + result = hosts._normalized_release('01.08.01234.1') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 1 + + def test_int_single_version_rc(self): + result = hosts._normalized_release('1rc-123') + assert result.int_major == 1 + assert result.int_minor == 0 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_single_version_with_trailing_space_rc(self): + result = hosts._normalized_release(' 1rc-123') + assert result.int_major == 1 + assert result.int_minor == 0 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_single_version_with_prepended_zero_rc(self): + result = hosts._normalized_release('01rc-123') + assert result.int_major == 1 + assert result.int_minor == 0 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_minor_version_rc(self): + result = hosts._normalized_release('1.8rc-123') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_minor_version_with_trailing_space_rc(self): + result = hosts._normalized_release(' 1.8rc-123') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_minor_version_with_prepended_zero_rc(self): + result = hosts._normalized_release('01.08rc-123') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 0 + assert result.int_garbage == 0 + + def test_int_patch_version_rc(self): + result = hosts._normalized_release('1.8.1234rc-123') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 0 + + def test_int_patch_version_with_trailing_space_rc(self): + result = hosts._normalized_release(' 1.8.1234rc-123') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 0 + + def test_int_patch_version_with_prepended_zero_rc(self): + result = hosts._normalized_release('01.08.01234rc-123') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 0 + + def test_int_garbage_version_rc(self): + result = hosts._normalized_release('1.8.1234.1rc-123') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 1 + + def test_int_garbage_version_with_trailing_space_rc(self): + result = hosts._normalized_release(' 1.8.1234.1rc-123') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 1 + + def test_int_garbage_version_with_prepended_zero_rc(self): + result = hosts._normalized_release('01.08.01234.1rc-1') + assert result.int_major == 1 + assert result.int_minor == 8 + assert result.int_patch == 1234 + assert result.int_garbage == 1 + + # with non ints + + def test_single_version(self): + result = hosts._normalized_release('1') + assert result.major == "1" + assert result.minor == "0" + assert result.patch == "0" + assert result.garbage == "0" + + def test_single_version_with_trailing_space(self): + result = hosts._normalized_release(' 1') + assert result.major == "1" + assert result.minor == "0" + assert result.patch == "0" + assert result.garbage == "0" + + def test_single_version_with_prepended_zero(self): + result = hosts._normalized_release('01') + assert result.major == "01" + assert result.minor == "0" + assert result.patch == "0" + assert result.garbage == "0" + + def test_minor_version(self): + result = hosts._normalized_release('1.8') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "0" + assert result.garbage == "0" + + def test_minor_version_with_trailing_space(self): + result = hosts._normalized_release(' 1.8') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "0" + assert result.garbage == "0" + + def test_minor_version_with_prepended_zero(self): + result = hosts._normalized_release('01.08') + assert result.major == "01" + assert result.minor == "08" + assert result.patch == "0" + assert result.garbage == "0" + + def test_patch_version(self): + result = hosts._normalized_release('1.8.1234') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "1234" + assert result.garbage == "0" + + def test_patch_version_with_trailing_space(self): + result = hosts._normalized_release(' 1.8.1234') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "1234" + assert result.garbage == "0" + + def test_patch_version_with_prepended_zero(self): + result = hosts._normalized_release('01.08.01234') + assert result.major == "01" + assert result.minor == "08" + assert result.patch == "01234" + assert result.garbage == "0" + + def test_garbage_version(self): + result = hosts._normalized_release('1.8.1234.1') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "1234" + assert result.garbage == "1" + + def test_garbage_version_with_trailing_space(self): + result = hosts._normalized_release(' 1.8.1234.1') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "1234" + assert result.garbage == "1" + + def test_garbage_version_with_prepended_zero(self): + result = hosts._normalized_release('01.08.01234.1') + assert result.major == "01" + assert result.minor == "08" + assert result.patch == "01234" + assert result.garbage == "1" + + def test_patch_version_rc(self): + result = hosts._normalized_release('1.8.1234rc-123') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "1234rc-123" + assert result.garbage == "0" + + def test_patch_version_with_trailing_space_rc(self): + result = hosts._normalized_release(' 1.8.1234rc-123') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "1234rc-123" + assert result.garbage == "0" + + def test_patch_version_with_prepended_zero_rc(self): + result = hosts._normalized_release('01.08.01234.1rc-123') + assert result.major == "01" + assert result.minor == "08" + assert result.patch == "01234" + assert result.garbage == "1rc-123" + + def test_garbage_version_rc(self): + result = hosts._normalized_release('1.8.1234.1rc-123') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "1234" + assert result.garbage == "1rc-123" + + def test_garbage_version_with_trailing_space_rc(self): + result = hosts._normalized_release(' 1.8.1234.1rc-123') + assert result.major == "1" + assert result.minor == "8" + assert result.patch == "1234" + assert result.garbage == "1rc-123" + + def test_garbage_version_with_prepended_zero_rc(self): + result = hosts._normalized_release('01.08.01234.1rc-1') + assert result.major == "01" + assert result.minor == "08" + assert result.patch == "01234" + assert result.garbage == "1rc-1" + + def test_garbage_version_with_no_numbers(self): + result = hosts._normalized_release('sid') + assert result.major == "sid" + assert result.minor == "0" + assert result.patch == "0" + assert result.garbage == "0" + + +class TestHostGet(object): + + def make_fake_connection(self, platform_information=None): + get_connection = Mock() + get_connection.return_value = get_connection + get_connection.remote_module.platform_information = Mock( + return_value=platform_information) + return get_connection + + def test_get_unsupported(self): + fake_get_connection = self.make_fake_connection(('Solaris Enterprise', '', '')) + with patch('ceph_deploy.hosts.get_connection', fake_get_connection): + with raises(exc.UnsupportedPlatform): + hosts.get('myhost') + + def test_get_unsupported_message(self): + fake_get_connection = self.make_fake_connection(('Solaris Enterprise', '', '')) + with patch('ceph_deploy.hosts.get_connection', fake_get_connection): + with raises(exc.UnsupportedPlatform) as error: + hosts.get('myhost') + + assert error.value.__str__() == 'Platform is not supported: Solaris Enterprise ' + + def test_get_unsupported_message_release(self): + fake_get_connection = self.make_fake_connection(('Solaris', 'Tijuana', '12')) + with patch('ceph_deploy.hosts.get_connection', fake_get_connection): + with raises(exc.UnsupportedPlatform) as error: + hosts.get('myhost') + + assert error.value.__str__() == 'Platform is not supported: Solaris 12 Tijuana' + + +class TestGetDistro(object): + + def test_get_debian(self): + result = hosts._get_distro('Debian') + assert result.__name__.endswith('debian') + + def test_get_ubuntu(self): + # Ubuntu imports debian stuff + result = hosts._get_distro('Ubuntu') + assert result.__name__.endswith('debian') + + def test_get_centos(self): + result = hosts._get_distro('CentOS') + assert result.__name__.endswith('centos') + + def test_get_scientific(self): + result = hosts._get_distro('Scientific') + assert result.__name__.endswith('centos') + + def test_get_oracle(self): + result = hosts._get_distro('Oracle Linux Server') + assert result.__name__.endswith('centos') + + def test_get_redhat(self): + result = hosts._get_distro('RedHat') + assert result.__name__.endswith('centos') + + def test_get_redhat_whitespace(self): + result = hosts._get_distro('Red Hat Enterprise Linux') + assert result.__name__.endswith('centos') + + def test_get_uknown(self): + assert hosts._get_distro('Solaris') is None + + def test_get_fallback(self): + result = hosts._get_distro('Solaris', 'Debian') + assert result.__name__.endswith('debian') + + def test_get_mint(self): + result = hosts._get_distro('LinuxMint') + assert result.__name__.endswith('debian') + + def test_get_virtuozzo(self): + result = hosts._get_distro('Virtuozzo Linux') + assert result.__name__.endswith('centos') + + def test_get_arch(self): + result = hosts._get_distro('Arch Linux') + assert result.__name__.endswith('arch') + + def test_get_altlinux(self): + result = hosts._get_distro('ALT Linux') + assert result.__name__.endswith('alt') + + def test_get_openeulerlinux(self): + result = hosts._get_distro('Openeuler') + assert result.__name__.endswith('centos') diff --git a/ceph_deploy/tests/unit/hosts/test_remotes.py b/ceph_deploy/tests/unit/hosts/test_remotes.py new file mode 100644 index 0000000..69ee4f7 --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_remotes.py @@ -0,0 +1,37 @@ +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + +from ceph_deploy.hosts import remotes + + +class TestObjectGrep(object): + + def setup(self): + self.file_object = StringIO('foo\n') + self.file_object.seek(0) + + def test_finds_term(self): + assert remotes.object_grep('foo', self.file_object) + + def test_does_not_find_anything(self): + assert remotes.object_grep('bar', self.file_object) is False + + +class TestWhich(object): + + def test_executable_is_a_directory(self, monkeypatch): + monkeypatch.setattr(remotes.os.path, 'exists', lambda x: True) + monkeypatch.setattr(remotes.os.path, 'isfile', lambda x: False) + assert remotes.which('foo') is None + + def test_executable_does_not_exist(self, monkeypatch): + monkeypatch.setattr(remotes.os.path, 'exists', lambda x: False) + monkeypatch.setattr(remotes.os.path, 'isfile', lambda x: True) + assert remotes.which('foo') is None + + def test_executable_exists_as_file(self, monkeypatch): + monkeypatch.setattr(remotes.os.path, 'exists', lambda x: True) + monkeypatch.setattr(remotes.os.path, 'isfile', lambda x: True) + assert remotes.which('foo') == '/usr/local/bin/foo' diff --git a/ceph_deploy/tests/unit/hosts/test_suse.py b/ceph_deploy/tests/unit/hosts/test_suse.py new file mode 100644 index 0000000..d3b3415 --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_suse.py @@ -0,0 +1,34 @@ +from ceph_deploy.hosts import suse +from ceph_deploy.hosts.suse.install import map_components, NON_SPLIT_PACKAGES + +class TestSuseInit(object): + def setup(self): + self.host = suse + + def test_choose_init_default(self): + self.host.release = None + init_type = self.host.choose_init(self.host) + assert init_type == "systemd" + + def test_choose_init_SLE_11(self): + self.host.release = '11' + init_type = self.host.choose_init(self.host) + assert init_type == "sysvinit" + + def test_choose_init_SLE_12(self): + self.host.release = '12' + init_type = self.host.choose_init(self.host) + assert init_type == "systemd" + + def test_choose_init_openSUSE_13_1(self): + self.host.release = '13.1' + init_type = self.host.choose_init(self.host) + assert init_type == "systemd" + +class TestSuseMapComponents(object): + def test_valid(self): + pkgs = map_components(NON_SPLIT_PACKAGES, ['ceph-osd', 'ceph-common', 'ceph-radosgw']) + assert 'ceph' in pkgs + assert 'ceph-common' in pkgs + assert 'ceph-radosgw' in pkgs + assert 'ceph-osd' not in pkgs diff --git a/ceph_deploy/tests/unit/hosts/test_util.py b/ceph_deploy/tests/unit/hosts/test_util.py new file mode 100644 index 0000000..c4a5947 --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_util.py @@ -0,0 +1,29 @@ +from ceph_deploy.hosts import util +from mock import Mock + + +class TestInstallYumPriorities(object): + + def setup(self): + self.distro = Mock() + self.patch_path = 'ceph_deploy.hosts.centos.install.pkg_managers.yum' + self.yum = Mock() + + def test_centos_six(self): + self.distro.release = ('6', '0') + self.distro.normalized_name = 'centos' + util.install_yum_priorities(self.distro, _yum=self.yum) + assert self.yum.call_args[0][1] == 'yum-plugin-priorities' + + def test_centos_five(self): + self.distro.release = ('5', '0') + self.distro.normalized_name = 'centos' + util.install_yum_priorities(self.distro, _yum=self.yum) + assert self.yum.call_args[0][1] == 'yum-priorities' + + def test_fedora(self): + self.distro.release = ('20', '0') + self.distro.normalized_name = 'fedora' + util.install_yum_priorities(self.distro, _yum=self.yum) + assert self.yum.call_args[0][1] == 'yum-plugin-priorities' + diff --git a/ceph_deploy/tests/unit/test_cli.py b/ceph_deploy/tests/unit/test_cli.py new file mode 100644 index 0000000..fff75fc --- /dev/null +++ b/ceph_deploy/tests/unit/test_cli.py @@ -0,0 +1,46 @@ +from ceph_deploy import cli +from ceph_deploy.tests import util + + +class FakeLogger(object): + + def __init__(self): + self._calls = [] + self._info = [] + + def _output(self): + return '\n'.join(self._calls) + + def _record(self, level, message): + self._calls.append(message) + method = getattr(self, '_%s' % level) + method.append(message) + + def info(self, message): + self._record('info', message) + + +class TestLogFlags(object): + + def setup(self): + self.logger = FakeLogger() + + def test_logs_multiple_object_attributes(self): + args = util.Empty(verbose=True, adjust_repos=False) + cli.log_flags(args, logger=self.logger) + result = self.logger._output() + assert ' verbose ' in result + assert ' adjust_repos ' in result + + def test_attributes_are_logged_with_values(self): + args = util.Empty(verbose=True) + cli.log_flags(args, logger=self.logger) + result = self.logger._output() + assert ' verbose ' in result + assert ' : True' in result + + def test_private_attributes_are_not_logged(self): + args = util.Empty(verbose=True, _private='some value') + cli.log_flags(args, logger=self.logger) + result = self.logger._output() + assert ' _private ' not in result diff --git a/ceph_deploy/tests/unit/test_conf.py b/ceph_deploy/tests/unit/test_conf.py new file mode 100644 index 0000000..685acbb --- /dev/null +++ b/ceph_deploy/tests/unit/test_conf.py @@ -0,0 +1,192 @@ +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO +from textwrap import dedent +import pytest +from mock import Mock, patch, mock_open +from ceph_deploy import conf + + +class TestLocateOrCreate(object): + + def setup(self): + self.fake_file = mock_open() + + def test_no_conf(self): + fake_path = Mock() + fake_path.exists = Mock(return_value=False) + with patch('ceph_deploy.conf.cephdeploy.open', self.fake_file, create=True): + with patch('ceph_deploy.conf.cephdeploy.path', fake_path): + conf.cephdeploy.location() + + assert self.fake_file.called is True + assert self.fake_file.call_args[0][0].endswith('/.cephdeploy.conf') + + def test_cwd_conf_exists(self): + fake_path = Mock() + fake_path.join = Mock(return_value='/srv/cephdeploy.conf') + fake_path.exists = Mock(return_value=True) + with patch('ceph_deploy.conf.cephdeploy.path', fake_path): + result = conf.cephdeploy.location() + + assert result == '/srv/cephdeploy.conf' + + def test_home_conf_exists(self): + fake_path = Mock() + fake_path.expanduser = Mock(return_value='/home/alfredo/.cephdeploy.conf') + fake_path.exists = Mock(side_effect=[False, True]) + with patch('ceph_deploy.conf.cephdeploy.path', fake_path): + result = conf.cephdeploy.location() + + assert result == '/home/alfredo/.cephdeploy.conf' + + +class TestConf(object): + + def test_has_repos(self): + cfg = conf.cephdeploy.Conf() + cfg.sections = lambda: ['foo'] + assert cfg.has_repos is True + + def test_has_no_repos(self): + cfg = conf.cephdeploy.Conf() + cfg.sections = lambda: ['ceph-deploy-install'] + assert cfg.has_repos is False + + def test_get_repos_is_empty(self): + cfg = conf.cephdeploy.Conf() + cfg.sections = lambda: ['ceph-deploy-install'] + assert cfg.get_repos() == [] + + def test_get_repos_is_not_empty(self): + cfg = conf.cephdeploy.Conf() + cfg.sections = lambda: ['ceph-deploy-install', 'foo'] + assert cfg.get_repos() == ['foo'] + + def test_get_safe_not_empty(self): + cfg = conf.cephdeploy.Conf() + cfg.get = lambda section, key: True + assert cfg.get_safe(1, 2) is True + + def test_get_safe_empty(self): + cfg = conf.cephdeploy.Conf() + assert cfg.get_safe(1, 2) is None + + +class TestConfGetList(object): + + def test_get_list_empty(self): + cfg = conf.cephdeploy.Conf() + conf_file = StringIO(dedent(""" + [foo] + key = + """)) + cfg.read_file(conf_file) + assert cfg.get_list('foo', 'key') == [''] + + def test_get_list_empty_when_no_key(self): + cfg = conf.cephdeploy.Conf() + conf_file = StringIO(dedent(""" + [foo] + """)) + cfg.read_file(conf_file) + assert cfg.get_list('foo', 'key') == [] + + def test_get_list_if_value_is_one_item(self): + cfg = conf.cephdeploy.Conf() + conf_file = StringIO(dedent(""" + [foo] + key = 1 + """)) + cfg.read_file(conf_file) + assert cfg.get_list('foo', 'key') == ['1'] + + def test_get_list_with_mutltiple_items(self): + cfg = conf.cephdeploy.Conf() + conf_file = StringIO(dedent(""" + [foo] + key = 1, 3, 4 + """)) + cfg.read_file(conf_file) + assert cfg.get_list('foo', 'key') == ['1', '3', '4'] + + def test_get_rid_of_comments(self): + cfg = conf.cephdeploy.Conf() + conf_file = StringIO(dedent(""" + [foo] + key = 1, 3, 4 # this is a wonderful comment y'all + """)) + cfg.read_file(conf_file) + assert cfg.get_list('foo', 'key') == ['1', '3', '4'] + + def test_get_rid_of_whitespace(self): + cfg = conf.cephdeploy.Conf() + conf_file = StringIO(dedent(""" + [foo] + key = 1, 3 , 4 + """)) + cfg.read_file(conf_file) + assert cfg.get_list('foo', 'key') == ['1', '3', '4'] + + def test_get_default_repo(self): + cfg = conf.cephdeploy.Conf() + conf_file = StringIO(dedent(""" + [foo] + default = True + """)) + cfg.read_file(conf_file) + assert cfg.get_default_repo() == 'foo' + + def test_get_default_repo_fails_non_truthy(self): + cfg = conf.cephdeploy.Conf() + conf_file = StringIO(dedent(""" + [foo] + default = 0 + """)) + cfg.read_file(conf_file) + assert cfg.get_default_repo() is False + + +truthy_values = ['yes', 'true', 'on'] +falsy_values = ['no', 'false', 'off'] + + +class TestSetOverrides(object): + + def setup(self): + self.args = Mock() + self.args.func.__name__ = 'foo' + self.conf = Mock() + + def test_override_global(self): + self.conf.sections = Mock(return_value=['ceph-deploy-global']) + self.conf.items = Mock(return_value=(('foo', 1),)) + arg_obj = conf.cephdeploy.set_overrides(self.args, self.conf) + assert arg_obj.foo == 1 + + def test_override_foo_section(self): + self.conf.sections = Mock( + return_value=['ceph-deploy-global', 'ceph-deploy-foo'] + ) + self.conf.items = Mock(return_value=(('bar', 1),)) + arg_obj = conf.cephdeploy.set_overrides(self.args, self.conf) + assert arg_obj.bar == 1 + + @pytest.mark.parametrize('value', truthy_values) + def test_override_truthy_values(self, value): + self.conf.sections = Mock( + return_value=['ceph-deploy-global', 'ceph-deploy-install'] + ) + self.conf.items = Mock(return_value=(('bar', value),)) + arg_obj = conf.cephdeploy.set_overrides(self.args, self.conf) + assert arg_obj.bar is True + + @pytest.mark.parametrize('value', falsy_values) + def test_override_falsy_values(self, value): + self.conf.sections = Mock( + return_value=['ceph-deploy-global', 'ceph-deploy-install'] + ) + self.conf.items = Mock(return_value=(('bar', value),)) + arg_obj = conf.cephdeploy.set_overrides(self.args, self.conf) + assert arg_obj.bar is False diff --git a/ceph_deploy/tests/unit/test_exc.py b/ceph_deploy/tests/unit/test_exc.py new file mode 100644 index 0000000..cd38686 --- /dev/null +++ b/ceph_deploy/tests/unit/test_exc.py @@ -0,0 +1,16 @@ +from pytest import raises +from ceph_deploy import exc + + +class TestExecutableNotFound(object): + + def test_executable_is_used(self): + with raises(exc.DeployError) as error: + raise exc.ExecutableNotFound('vim', 'node1') + assert "'vim'" in str(error) + + def test_host_is_used(self): + with raises(exc.DeployError) as error: + raise exc.ExecutableNotFound('vim', 'node1') + assert "node1" in str(error) + diff --git a/ceph_deploy/tests/unit/test_mon.py b/ceph_deploy/tests/unit/test_mon.py new file mode 100644 index 0000000..012a6b6 --- /dev/null +++ b/ceph_deploy/tests/unit/test_mon.py @@ -0,0 +1,224 @@ +import sys +import py.test +from mock import Mock, patch +# the below import of mock again is to workaround a py.test issue: +# https://github.com/pytest-dev/pytest/issues/1035 +import mock +from ceph_deploy import mon +from ceph_deploy.hosts.common import mon_create +from ceph_deploy.misc import mon_hosts, remote_shortname + + +def path_exists(target_paths=None): + """ + A quick helper that enforces a check for the existence of a path. Since we + are dealing with fakes, we allow to pass in a list of paths that are OK to + return True, otherwise return False. + """ + target_paths = target_paths or [] + + def exists(path): + return path in target_paths + return exists + + +@py.test.mark.skipif(reason='failing due to removal of pushy') +class TestCreateMon(object): + + def setup(self): + # this setup is way more verbose than normal + # but we are forced to because this function needs a lot + # passed in for remote execution. No other way around it. + self.socket = Mock() + self.socket.gethostname.return_value = 'hostname' + self.fake_file = mock.mock_open() + self.distro = Mock() + self.sprocess = Mock() + self.paths = Mock() + self.paths.mon.path = Mock(return_value='/cluster-hostname') + self.logger = Mock() + self.logger.info = self.logger.debug = lambda x: sys.stdout.write(str(x) + "\n") + + def test_create_mon_tmp_path_if_nonexistent(self): + self.distro.sudo_conn.modules.os.path.exists = Mock( + side_effect=path_exists(['/cluster-hostname'])) + self.paths.mon.constants.tmp_path = '/var/lib/ceph/tmp' + args = Mock(return_value=['cluster', '1234', 'initd']) + args.cluster = 'cluster' + with patch('ceph_deploy.hosts.common.conf.load'): + mon_create(self.distro, args, Mock(), 'hostname') + + result = self.distro.conn.remote_module.create_mon_path.call_args_list[-1] + assert result ==mock.call('/var/lib/ceph/mon/cluster-hostname') + + def test_write_keyring(self): + self.distro.sudo_conn.modules.os.path.exists = Mock( + side_effect=path_exists(['/'])) + args = Mock(return_value=['cluster', '1234', 'initd']) + args.cluster = 'cluster' + with patch('ceph_deploy.hosts.common.conf.load'): + with patch('ceph_deploy.hosts.common.remote') as fake_remote: + mon_create(self.distro, self.logger, args, Mock(), 'hostname') + + # the second argument to `remote()` should be the write func + result = fake_remote.call_args_list[1][0][-1].__name__ + assert result == 'write_monitor_keyring' + + def test_write_done_path(self): + self.distro.sudo_conn.modules.os.path.exists = Mock( + side_effect=path_exists(['/'])) + args = Mock(return_value=['cluster', '1234', 'initd']) + args.cluster = 'cluster' + + with patch('ceph_deploy.hosts.common.conf.load'): + with patch('ceph_deploy.hosts.common.remote') as fake_remote: + mon_create(self.distro, self.logger, args, Mock(), 'hostname') + + # the second to last argument to `remote()` should be the done path + # write + result = fake_remote.call_args_list[-2][0][-1].__name__ + assert result == 'create_done_path' + + def test_write_init_path(self): + self.distro.sudo_conn.modules.os.path.exists = Mock( + side_effect=path_exists(['/'])) + args = Mock(return_value=['cluster', '1234', 'initd']) + args.cluster = 'cluster' + + with patch('ceph_deploy.hosts.common.conf.load'): + with patch('ceph_deploy.hosts.common.remote') as fake_remote: + mon_create(self.distro, self.logger, args, Mock(), 'hostname') + + result = fake_remote.call_args_list[-1][0][-1].__name__ + assert result == 'create_init_path' + + def test_mon_hosts(self): + hosts = Mock() + for (name, host) in mon_hosts(('name1', 'name2.localdomain', + 'name3:1.2.3.6', 'name4:localhost.localdomain')): + hosts.get(name, host) + + expected = [mock.call.get('name1', 'name1'), + mock.call.get('name2', 'name2.localdomain'), + mock.call.get('name3', '1.2.3.6'), + mock.call.get('name4', 'localhost.localdomain')] + result = hosts.mock_calls + assert result == expected + + def test_remote_shortname_fqdn(self): + socket = Mock() + socket.gethostname.return_value = 'host.f.q.d.n' + assert remote_shortname(socket) == 'host' + + def test_remote_shortname_host(self): + socket = Mock() + socket.gethostname.return_value = 'host' + assert remote_shortname(socket) == 'host' + + +@py.test.mark.skipif(reason='failing due to removal of pushy') +class TestIsRunning(object): + + def setup(self): + self.fake_popen = Mock() + self.fake_popen.return_value = self.fake_popen + + def test_is_running_centos(self): + centos_out = ['', "mon.mire094: running {'version': '0.6.15'}"] + self.fake_popen.communicate = Mock(return_value=centos_out) + with patch('ceph_deploy.mon.subprocess.Popen', self.fake_popen): + result = mon.is_running(['ceph', 'status']) + assert result is True + + def test_is_not_running_centos(self): + centos_out = ['', "mon.mire094: not running {'version': '0.6.15'}"] + self.fake_popen.communicate = Mock(return_value=centos_out) + with patch('ceph_deploy.mon.subprocess.Popen', self.fake_popen): + result = mon.is_running(['ceph', 'status']) + assert result is False + + def test_is_dead_centos(self): + centos_out = ['', "mon.mire094: dead {'version': '0.6.15'}"] + self.fake_popen.communicate = Mock(return_value=centos_out) + with patch('ceph_deploy.mon.subprocess.Popen', self.fake_popen): + result = mon.is_running(['ceph', 'status']) + assert result is False + + def test_is_running_ubuntu(self): + ubuntu_out = ['', "ceph-mon (ceph/mira103) start/running, process 5866"] + self.fake_popen.communicate = Mock(return_value=ubuntu_out) + with patch('ceph_deploy.mon.subprocess.Popen', self.fake_popen): + result = mon.is_running(['ceph', 'status']) + assert result is True + + def test_is_not_running_ubuntu(self): + ubuntu_out = ['', "ceph-mon (ceph/mira103) start/dead, process 5866"] + self.fake_popen.communicate = Mock(return_value=ubuntu_out) + with patch('ceph_deploy.mon.subprocess.Popen', self.fake_popen): + result = mon.is_running(['ceph', 'status']) + assert result is False + + def test_is_dead_ubuntu(self): + ubuntu_out = ['', "ceph-mon (ceph/mira103) stop/not running, process 5866"] + self.fake_popen.communicate = Mock(return_value=ubuntu_out) + with patch('ceph_deploy.mon.subprocess.Popen', self.fake_popen): + result = mon.is_running(['ceph', 'status']) + assert result is False + + +class TestKeyringParser(object): + + def test_line_ends_with_newline_char(self, tmpdir): + keyring = tmpdir.join('foo.mon.keyring') + keyring.write('[section]\nasdfasdf\nkey = value') + result = mon.keyring_parser(keyring.strpath) + + assert result == ['section'] + + def test_line_does_not_end_with_newline_char(self, tmpdir): + keyring = tmpdir.join('foo.mon.keyring') + keyring.write('[section]asdfasdf\nkey = value') + result = mon.keyring_parser(keyring.strpath) + + assert result == [] + + +class TestConcatenateKeyrings(object): + + def setup(self): + self.args = Mock() + + def make_keyring(self, tmpdir, name, contents): + keyring = tmpdir.join(name) + keyring.write(contents) + return keyring + + def test_multiple_keyrings_work(self, tmpdir): + self.make_keyring(tmpdir, 'foo.keyring', '[mon.1]\nkey = value\n') + self.make_keyring(tmpdir, 'bar.keyring', '[mon.2]\nkey = value\n') + self.make_keyring(tmpdir, 'fez.keyring', '[mon.3]\nkey = value\n') + self.args.keyrings = tmpdir.strpath + result = mon.concatenate_keyrings(self.args).split('\n') + assert '[mon.2]' in result + assert 'key = value' in result + assert '[mon.3]' in result + assert 'key = value' in result + assert '[mon.1]' in result + assert 'key = value' in result + + def test_skips_duplicate_content(self, tmpdir): + self.make_keyring(tmpdir, 'foo.keyring', '[mon.1]\nkey = value\n') + self.make_keyring(tmpdir, 'bar.keyring', '[mon.2]\nkey = value\n') + self.make_keyring(tmpdir, 'fez.keyring', '[mon.3]\nkey = value\n') + self.make_keyring(tmpdir, 'dupe.keyring', '[mon.3]\nkey = value\n') + self.args.keyrings = tmpdir.strpath + result = mon.concatenate_keyrings(self.args).split('\n') + assert result.count('[mon.3]') == 1 + assert result.count('[mon.2]') == 1 + assert result.count('[mon.1]') == 1 + + def test_errors_when_no_keyrings(self, tmpdir): + self.args.keyrings = tmpdir.strpath + + with py.test.raises(RuntimeError): + mon.concatenate_keyrings(self.args) diff --git a/ceph_deploy/tests/unit/test_new.py b/ceph_deploy/tests/unit/test_new.py new file mode 100644 index 0000000..a32b2ea --- /dev/null +++ b/ceph_deploy/tests/unit/test_new.py @@ -0,0 +1,28 @@ +from ceph_deploy import new +from ceph_deploy.tests import util +import pytest + + +class TestValidateHostIp(object): + + def test_for_all_subnets_all_ips_match(self): + ips = util.generate_ips("10.0.0.1", "10.0.0.40") + ips.extend(util.generate_ips("10.0.1.1", "10.0.1.40")) + subnets = ["10.0.0.1/16", "10.0.1.1/16"] + assert new.validate_host_ip(ips, subnets) is None + + def test_all_subnets_have_one_matching_ip(self): + ips = util.generate_ips("10.0.0.1", "10.0.0.40") + ips.extend(util.generate_ips("10.0.1.1", "10.0.1.40")) + # regardless of extra IPs that may not match. The requirement + # is already satisfied + ips.extend(util.generate_ips("10.1.2.1", "10.1.2.40")) + subnets = ["10.0.0.1/16", "10.0.1.1/16"] + assert new.validate_host_ip(ips, subnets) is None + + def test_not_all_subnets_have_one_matching_ip(self): + ips = util.generate_ips("10.0.0.1", "10.0.0.40") + ips.extend(util.generate_ips("10.0.1.1", "10.0.1.40")) + subnets = ["10.0.0.1/16", "10.1.1.1/16"] + with pytest.raises(RuntimeError): + new.validate_host_ip(ips, subnets) diff --git a/ceph_deploy/tests/unit/util/test_arg_validators.py b/ceph_deploy/tests/unit/util/test_arg_validators.py new file mode 100644 index 0000000..8acb712 --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_arg_validators.py @@ -0,0 +1,128 @@ +import socket +from mock import Mock +from argparse import ArgumentError +from pytest import raises + +from ceph_deploy.util import arg_validators + + +class TestRegexMatch(object): + + def test_match_raises(self): + validator = arg_validators.RegexMatch(r'\d+') + with raises(ArgumentError): + validator('1') + + def test_match_passes(self): + validator = arg_validators.RegexMatch(r'\d+') + assert validator('foo') == 'foo' + + def test_default_error_message(self): + validator = arg_validators.RegexMatch(r'\d+') + with raises(ArgumentError) as error: + validator('1') + message = error.value.message + assert message == 'must match pattern \d+' + + def test_custom_error_message(self): + validator = arg_validators.RegexMatch(r'\d+', 'wat') + with raises(ArgumentError) as error: + validator('1') + message = error.value.message + assert message == 'wat' + + +class TestHostName(object): + + def setup(self): + self.fake_sock = Mock() + self.fake_sock.gaierror = socket.gaierror + self.fake_sock.getaddrinfo.side_effect = socket.gaierror + + def test_hostname_is_not_resolvable(self): + hostname = arg_validators.Hostname(self.fake_sock) + with raises(ArgumentError) as error: + hostname('unresolvable') + message = error.value.message + assert 'is not resolvable' in message + + def test_hostname_with_name_is_not_resolvable(self): + hostname = arg_validators.Hostname(self.fake_sock) + with raises(ArgumentError) as error: + hostname('name:foo') + message = error.value.message + assert 'foo is not resolvable' in message + + def test_ip_is_allowed_when_paired_with_host(self): + self.fake_sock = Mock() + self.fake_sock.gaierror = socket.gaierror + + def side_effect(*args): + # First call passes, second call raises socket.gaierror + self.fake_sock.getaddrinfo.side_effect = socket.gaierror + + self.fake_sock.getaddrinfo.side_effect = side_effect + hostname = arg_validators.Hostname(self.fake_sock) + result = hostname('name:192.168.1.111') + assert result == 'name:192.168.1.111' + + def test_ipv6_is_allowed_when_paired_with_host(self): + self.fake_sock = Mock() + self.fake_sock.gaierror = socket.gaierror + + def side_effect(*args): + # First call passes, second call raises socket.gaierror + self.fake_sock.getaddrinfo.side_effect = socket.gaierror + + self.fake_sock.getaddrinfo.side_effect = side_effect + hostname = arg_validators.Hostname(self.fake_sock) + result = hostname('name:2001:0db8:85a3:0000:0000:8a2e:0370:7334') + assert result == 'name:2001:0db8:85a3:0000:0000:8a2e:0370:7334' + + def test_host_is_resolvable(self): + self.fake_sock = Mock() + self.fake_sock.gaierror = socket.gaierror + + def side_effect(*args): + # First call passes, second call raises socket.gaierror + self.fake_sock.getaddrinfo.side_effect = socket.gaierror + + self.fake_sock.getaddrinfo.side_effect = side_effect + hostname = arg_validators.Hostname(self.fake_sock) + result = hostname('name:example.com') + assert result == 'name:example.com' + + def test_hostname_must_be_an_ip(self): + self.fake_sock.getaddrinfo = Mock() + hostname = arg_validators.Hostname(self.fake_sock) + with raises(ArgumentError) as error: + hostname('0') + message = error.value.message + assert '0 must be a hostname' in message + + +class TestSubnet(object): + + def test_subnet_has_less_than_four_numbers(self): + validator = arg_validators.Subnet() + + with raises(ArgumentError) as error: + validator('3.3.3/12') + message = error.value.message + assert 'at least 4 numbers' in message + + def test_subnet_has_non_digits(self): + validator = arg_validators.Subnet() + + with raises(ArgumentError) as error: + validator('3.3.3.a/12') + message = error.value.message + assert 'have digits separated by dots' in message + + def test_subnet_missing_slash(self): + validator = arg_validators.Subnet() + + with raises(ArgumentError) as error: + validator('3.3.3.3') + message = error.value.message + assert 'must contain a slash' in message diff --git a/ceph_deploy/tests/unit/util/test_constants.py b/ceph_deploy/tests/unit/util/test_constants.py new file mode 100644 index 0000000..ce32a57 --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_constants.py @@ -0,0 +1,16 @@ +from ceph_deploy.util import constants + + +class TestPaths(object): + + def test_mon_path(self): + assert constants.mon_path.startswith('/') + assert constants.mon_path.endswith('/mon') + + def test_mds_path(self): + assert constants.mds_path.startswith('/') + assert constants.mds_path.endswith('/mds') + + def test_tmp_path(self): + assert constants.tmp_path.startswith('/') + assert constants.tmp_path.endswith('/tmp') diff --git a/ceph_deploy/tests/unit/util/test_net.py b/ceph_deploy/tests/unit/util/test_net.py new file mode 100644 index 0000000..9c71bad --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_net.py @@ -0,0 +1,53 @@ +try: + from urllib.error import HTTPError +except ImportError: + from urllib2 import HTTPError + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from ceph_deploy.util import net +from ceph_deploy.tests import util +import pytest + + +# The following class adds about 1900 tests via py.test generation + +class TestIpInSubnet(object): + + @pytest.mark.parametrize('ip', util.generate_ips("10.0.0.1", "10.0.0.255")) + def test_correct_for_10_0_0_255(self, ip): + assert net.ip_in_subnet(ip, "10.0.0.0/16") + + @pytest.mark.parametrize('ip', util.generate_ips("10.0.0.1", "10.0.0.255")) + def test_false_for_10_0_0_255(self, ip): + assert net.ip_in_subnet(ip, "10.2.0.0/24") is False + + @pytest.mark.parametrize('ip', util.generate_ips("255.255.255.1", "255.255.255.255")) + def test_false_for_255_addresses(self, ip): + assert net.ip_in_subnet(ip, "10.9.1.0/16") is False + + @pytest.mark.parametrize('ip', util.generate_ips("172.7.1.1", "172.7.1.255")) + def test_false_for_172_addresses(self, ip): + assert net.ip_in_subnet(ip, "172.3.0.0/16") is False + + @pytest.mark.parametrize('ip', util.generate_ips("10.9.8.0", "10.9.8.255")) + def test_true_for_16_subnets(self, ip): + assert net.ip_in_subnet(ip, "10.9.1.0/16") is True + + @pytest.mark.parametrize('ip', util.generate_ips("10.9.8.0", "10.9.8.255")) + def test_false_for_24_subnets(self, ip): + assert net.ip_in_subnet(ip, "10.9.1.0/24") is False + + +class TestGetRequest(object): + + def test_urlopen_fails(self, monkeypatch): + def bad_urlopen(url): + raise HTTPError('url', 500, 'error', '', StringIO()) + + monkeypatch.setattr(net, 'urlopen', bad_urlopen) + with pytest.raises(RuntimeError): + net.get_request('https://example.ceph.com') diff --git a/ceph_deploy/tests/unit/util/test_packages.py b/ceph_deploy/tests/unit/util/test_packages.py new file mode 100644 index 0000000..73cc3e8 --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_packages.py @@ -0,0 +1,43 @@ +from mock import Mock, patch +from ceph_deploy.exc import ExecutableNotFound +from ceph_deploy.util import packages + + +class TestCephIsInstalled(object): + + def test_installed(self): + with patch('ceph_deploy.util.packages.system'): + c = packages.Ceph(Mock()) + assert c.installed is True + + def test_not_installed(self): + with patch('ceph_deploy.util.packages.system') as fsystem: + bad_executable = Mock( + side_effect=ExecutableNotFound('host', 'ceph') + ) + fsystem.executable_path = bad_executable + c = packages.Ceph(Mock()) + assert c.installed is False + + +class TestCephVersion(object): + + def test_executable_not_found(self): + with patch('ceph_deploy.util.packages.system') as fsystem: + bad_executable = Mock( + side_effect=ExecutableNotFound('host', 'ceph') + ) + fsystem.executable_path = bad_executable + c = packages.Ceph(Mock()) + assert c._get_version_output() == '' + + def test_output_is_unusable(self): + _check = Mock(return_value=(b'', b'', 1)) + c = packages.Ceph(Mock(), _check=_check) + assert c._get_version_output() == '' + + def test_output_usable(self): + version = b'ceph version 9.0.1-kjh234h123hd (asdf78asdjh234)' + _check = Mock(return_value=(version, b'', 1)) + c = packages.Ceph(Mock(), _check=_check) + assert c._get_version_output() == '9.0.1-kjh234h123hd' diff --git a/ceph_deploy/tests/unit/util/test_paths.py b/ceph_deploy/tests/unit/util/test_paths.py new file mode 100644 index 0000000..64ff906 --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_paths.py @@ -0,0 +1,50 @@ +from ceph_deploy.util import paths + + +class TestMonPaths(object): + + def test_base_path(self): + result = paths.mon.base('mycluster') + assert result.endswith('/mycluster-') + + def test_path(self): + result = paths.mon.path('mycluster', 'myhostname') + assert result.startswith('/') + assert result.endswith('/mycluster-myhostname') + + def test_done(self): + result = paths.mon.done('mycluster', 'myhostname') + assert result.startswith('/') + assert result.endswith('mycluster-myhostname/done') + + def test_init(self): + result = paths.mon.init('mycluster', 'myhostname', 'init') + assert result.startswith('/') + assert result.endswith('mycluster-myhostname/init') + + def test_keyring(self): + result = paths.mon.keyring('mycluster', 'myhostname') + assert result.startswith('/') + assert result.endswith('tmp/mycluster-myhostname.mon.keyring') + + def test_asok(self): + result = paths.mon.asok('mycluster', 'myhostname') + assert result.startswith('/') + assert result.endswith('mycluster-mon.myhostname.asok') + + def test_monmap(self): + result = paths.mon.monmap('mycluster', 'myhostname') + assert result.startswith('/') + assert result.endswith('tmp/mycluster.myhostname.monmap') + + def test_gpg_url_release(self): + result = paths.gpg.url('release') + assert result == "https://download.ceph.com/keys/release.asc" + + def test_gpg_url_autobuild(self): + result = paths.gpg.url('autobuild') + assert result == "https://download.ceph.com/keys/autobuild.asc" + + def test_gpg_url_http(self): + result = paths.gpg.url('release', protocol="http") + assert result == "http://download.ceph.com/keys/release.asc" diff --git a/ceph_deploy/tests/unit/util/test_pkg_managers.py b/ceph_deploy/tests/unit/util/test_pkg_managers.py new file mode 100644 index 0000000..5f06ad0 --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_pkg_managers.py @@ -0,0 +1,195 @@ +from mock import patch, Mock +from ceph_deploy.util import pkg_managers + + +class TestApt(object): + + def setup(self): + self.to_patch = 'ceph_deploy.util.pkg_managers.remoto.process.run' + + def test_install_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Apt(Mock()).install('vim') + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_install_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Apt(Mock()).install(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + + def test_remove_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Apt(Mock()).remove('vim') + result = fake_run.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_remove_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Apt(Mock()).remove(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + + +class TestYum(object): + + def setup(self): + self.to_patch = 'ceph_deploy.util.pkg_managers.remoto.process.run' + + def test_install_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Yum(Mock()).install('vim') + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_install_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Yum(Mock()).install(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + + def test_remove_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Yum(Mock()).remove('vim') + result = fake_run.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_remove_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Yum(Mock()).remove(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + + +class TestZypper(object): + + def setup(self): + self.to_patch = 'ceph_deploy.util.pkg_managers.remoto.process.run' + self.to_check = 'ceph_deploy.util.pkg_managers.remoto.process.check' + + def test_install_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Zypper(Mock()).install('vim') + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_install_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.Zypper(Mock()).install(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + + def test_remove_single_package(self): + fake_check = Mock() + fake_check.return_value = '', '', 0 + with patch(self.to_check, fake_check): + pkg_managers.Zypper(Mock()).remove('vim') + result = fake_check.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_remove_multiple_packages(self): + fake_check = Mock() + fake_check.return_value = '', '', 0 + with patch(self.to_check, fake_check): + pkg_managers.Zypper(Mock()).remove(['vim', 'zsh']) + result = fake_check.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + + +class TestDNF(object): + + def setup(self): + self.to_patch = 'ceph_deploy.util.pkg_managers.remoto.process.run' + + def test_install_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.DNF(Mock()).install('vim') + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_install_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.DNF(Mock()).install(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + + def test_remove_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.DNF(Mock()).remove('vim') + result = fake_run.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_remove_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.DNF(Mock()).remove(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'remove' in result[0][-1] + + +class TestAtpRpm(object): + + def setup(self): + self.to_patch = 'ceph_deploy.util.pkg_managers.remoto.process.run' + + def test_install_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.AptRpm(Mock()).install('vim') + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_install_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.AptRpm(Mock()).install(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'install' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + + def test_remove_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.AptRpm(Mock()).remove('vim') + result = fake_run.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-1] == 'vim' + + def test_remove_multiple_packages(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.AptRpm(Mock()).remove(['vim', 'zsh']) + result = fake_run.call_args_list[-1] + assert 'remove' in result[0][-1] + assert result[0][-1][-2:] == ['vim', 'zsh'] + diff --git a/ceph_deploy/tests/unit/util/test_system.py b/ceph_deploy/tests/unit/util/test_system.py new file mode 100644 index 0000000..73ca546 --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_system.py @@ -0,0 +1,57 @@ +from mock import Mock +from pytest import raises +from ceph_deploy.util import system +from ceph_deploy import exc + + +class TestExecutablePath(object): + + def test_returns_path(self): + fake_conn = Mock() + fake_conn.remote_module.which = Mock(return_value='/path') + result = system.executable_path(fake_conn, 'foo') + assert result == '/path' + + def test_cannot_find_executable(self): + fake_conn = Mock() + fake_conn.remote_module.which = Mock(return_value=None) + with raises(exc.ExecutableNotFound): + system.executable_path(fake_conn, 'foo') + + +class TestIsUpstart(object): + + def test_it_is_actually_systemd(self): + fake_conn = Mock() + fake_conn.remote_module.grep = Mock(return_value=True) + result = system.is_upstart(fake_conn) + assert result is False + + def test_no_initctl(self): + fake_conn = Mock() + fake_conn.remote_module.grep = Mock(return_value=False) + fake_conn.remote_module.which = Mock(return_value=None) + result = system.is_upstart(fake_conn) + assert result is False + + def test_initctl_version_says_upstart(self, monkeypatch): + fake_conn = Mock() + fake_conn.remote_module.grep = Mock(return_value=False) + fake_conn.remote_module.which = Mock(return_value='/bin/initctl') + fake_stdout = ([b'init', b'(upstart 1.12.1)'], [], 0) + fake_check = Mock(return_value=fake_stdout) + monkeypatch.setattr("ceph_deploy.util.system.remoto.process.check", lambda *a: fake_check()) + + result = system.is_upstart(fake_conn) + assert result is True + + def test_initctl_version_says_something_else(self, monkeypatch): + fake_conn = Mock() + fake_conn.remote_module.grep = Mock(return_value=False) + fake_conn.remote_module.which = Mock(return_value='/bin/initctl') + fake_stdout = ([b'nosh', b'version', b'1.14'], [], 0) + fake_check = Mock(return_value=fake_stdout) + monkeypatch.setattr("ceph_deploy.util.system.remoto.process.check", lambda *a: fake_check()) + + result = system.is_upstart(fake_conn) + assert result is False diff --git a/ceph_deploy/tests/unit/util/test_templates.py b/ceph_deploy/tests/unit/util/test_templates.py new file mode 100644 index 0000000..36c39cd --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_templates.py @@ -0,0 +1,29 @@ +from textwrap import dedent +from ceph_deploy.util import templates + + +class TestCustomRepo(object): + + def test_only_repo_name(self): + result = templates.custom_repo(reponame='foo') + assert result == '[foo]' + + def test_second_line_with_good_value(self): + result = templates.custom_repo(reponame='foo', enabled=0) + assert result == '[foo]\nenabled=0' + + def test_mixed_values(self): + result = templates.custom_repo( + reponame='foo', + enabled=0, + gpgcheck=1, + baseurl='example.org') + assert result == dedent("""\ + [foo] + baseurl=example.org + enabled=0 + gpgcheck=1""") + + def test_allow_invalid_options(self): + result = templates.custom_repo(reponame='foo', bar='bar') + assert result == '[foo]' diff --git a/ceph_deploy/tests/util.py b/ceph_deploy/tests/util.py new file mode 100644 index 0000000..50932da --- /dev/null +++ b/ceph_deploy/tests/util.py @@ -0,0 +1,33 @@ + + +def generate_ips(start_ip, end_ip): + start = list(map(int, start_ip.split("."))) + end = list(map(int, end_ip.split("."))) + temp = start + ip_range = [] + + ip_range.append(start_ip) + while temp != end: + start[3] += 1 + for i in (3, 2, 1): + if temp[i] == 256: + temp[i] = 0 + temp[i-1] += 1 + ip_range.append(".".join(map(str, temp))) + + return ip_range + + +class Empty(object): + """ + A bare class, with explicit behavior for key/value items to be set at + instantiation. + """ + def __init__(self, **kw): + for k, v in kw.items(): + setattr(self, k, v) + + +def assert_too_few_arguments(err): + assert ("error: too few arguments" in err or + "error: the following argument" in err) diff --git a/ceph_deploy/util/__init__.py b/ceph_deploy/util/__init__.py new file mode 100644 index 0000000..a7d0838 --- /dev/null +++ b/ceph_deploy/util/__init__.py @@ -0,0 +1,11 @@ + + +def as_string(string): + """ + Ensure that whatever type of string is incoming, it is returned as an + actual string, versus 'bytes' which Python 3 likes to use. + """ + if isinstance(string, bytes): + # we really ignore here if we can't properly decode with utf-8 + return string.decode('utf-8', 'ignore') + return string diff --git a/ceph_deploy/util/arg_validators.py b/ceph_deploy/util/arg_validators.py new file mode 100644 index 0000000..e61fcd1 --- /dev/null +++ b/ceph_deploy/util/arg_validators.py @@ -0,0 +1,83 @@ +import socket +import argparse +import re + + +class RegexMatch(object): + """ + Performs regular expression match on value. + If the regular expression pattern matches it will it will return an error + message that will work with argparse. + """ + + def __init__(self, pattern, statement=None): + self.string_pattern = pattern + self.pattern = re.compile(pattern) + self.statement = statement + if not self.statement: + self.statement = "must match pattern %s" % self.string_pattern + + def __call__(self, string): + match = self.pattern.search(string) + if match: + raise argparse.ArgumentError(None, self.statement) + return string + + +class Hostname(object): + """ + Checks wether a given hostname is resolvable in DNS, otherwise raising and + argparse error. + """ + + def __init__(self, _socket=None): + self.socket = _socket or socket # just used for testing + + def __call__(self, string): + parts = string.split(':', 1) + name = parts[0] + host = parts[-1] + try: + self.socket.getaddrinfo(host, 0) + except self.socket.gaierror: + msg = "hostname: %s is not resolvable" % host + raise argparse.ArgumentError(None, msg) + + try: + self.socket.getaddrinfo(name, 0, 0, 0, 0, self.socket.AI_NUMERICHOST) + except self.socket.gaierror: + return string # not an IP + else: + msg = '%s must be a hostname not an IP' % name + raise argparse.ArgumentError(None, msg) + + return string + + +class Subnet(object): + """ + A really dumb validator to ensure that we are receiving a subnet (or + something that actually looks like a subnet). + + It doesn't enforce at all the constraints of proper validation as that has + its own set of caveats that are difficult to implement given that + ceph-deploy doesn't (should not) include third party dependencies. + """ + + def __call__(self, string): + ip = string.split('/')[0] + ip_parts = ip.split('.') + + if len(ip_parts) != 4: + err = "subnet must have at least 4 numbers separated by dots like x.x.x.x/xx, but got: %s" % string + raise argparse.ArgumentError(None, err) + + if [i for i in ip_parts[:4] if i.isalpha()]: # only numbers + err = "subnet must have digits separated by dots like x.x.x.x/xx, but got: %s" % string + raise argparse.ArgumentError(None, err) + + if len(string.split('/')) != 2: + err = "subnet must contain a slash, like x.x.x.x/xx, but got: %s" % string + raise argparse.ArgumentError(None, err) + + return string diff --git a/ceph_deploy/util/constants.py b/ceph_deploy/util/constants.py new file mode 100644 index 0000000..a76dcd6 --- /dev/null +++ b/ceph_deploy/util/constants.py @@ -0,0 +1,36 @@ +from os.path import join +from collections import namedtuple + +# Base Path for ceph +base_path = '/var/lib/ceph' + +# Base run Path +base_run_path = '/var/run/ceph' + +tmp_path = join(base_path, 'tmp') + +mon_path = join(base_path, 'mon') + +mgr_path = join(base_path, 'mgr') + +mds_path = join(base_path, 'mds') + +osd_path = join(base_path, 'osd') + +# Default package components to install +_base_components = [ + 'ceph', + 'ceph-osd', + 'ceph-mds', + 'ceph-mon', +] + +default_components = namedtuple('DefaultComponents', ['rpm', 'deb', 'pkgtarxz']) + +# the difference here is because RPMs currently name the radosgw differently than DEBs. +# TODO: This needs to get unified once the packaging naming gets consistent +default_components.rpm = tuple(_base_components + ['ceph-radosgw']) +default_components.deb = tuple(_base_components + ['radosgw']) +default_components.pkgtarxz = tuple(['ceph']) + +gpg_key_base_url = "download.ceph.com/keys/" diff --git a/ceph_deploy/util/decorators.py b/ceph_deploy/util/decorators.py new file mode 100644 index 0000000..70e002a --- /dev/null +++ b/ceph_deploy/util/decorators.py @@ -0,0 +1,112 @@ +import logging +import sys +import traceback +from functools import wraps + + +def catches(catch=None, handler=None, exit=True, handle_all=False): + """ + Very simple decorator that tries any of the exception(s) passed in as + a single exception class or tuple (containing multiple ones) returning the + exception message and optionally handling the problem if it raises with the + handler if it is provided. + + So instead of doing something like this:: + + def bar(): + try: + some_call() + print "Success!" + except TypeError, exc: + print "Error while handling some call: %s" % exc + sys.exit(1) + + You would need to decorate it like this to have the same effect:: + + @catches(TypeError) + def bar(): + some_call() + print "Success!" + + If multiple exceptions need to be caught they need to be provided as a + tuple:: + + @catches((TypeError, AttributeError)) + def bar(): + some_call() + print "Success!" + + If adding a handler, it should accept a single argument, which would be the + exception that was raised, it would look like:: + + def my_handler(exc): + print 'Handling exception %s' % str(exc) + raise SystemExit + + @catches(KeyboardInterrupt, handler=my_handler) + def bar(): + some_call() + + Note that the handler needs to raise its SystemExit if it wants to halt + execution, otherwise the decorator would continue as a normal try/except + block. + + + :param catch: A tuple with one (or more) Exceptions to catch + :param handler: Optional handler to have custom handling of exceptions + :param exit: Raise a ``SystemExit`` after handling exceptions + :param handle_all: Handle all other exceptions via logging. + """ + catch = catch or Exception + logger = logging.getLogger('ceph_deploy') + + def decorate(f): + + @wraps(f) + def newfunc(*a, **kw): + exit_from_catch = False + try: + return f(*a, **kw) + except catch as e: + if handler: + return handler(e) + else: + logger.error(make_exception_message(e)) + + if exit: + exit_from_catch = True + sys.exit(1) + except Exception: # anything else, no need to save the exception as a variable + if handle_all is False: # re-raise if we are not supposed to handle everything + raise + # Make sure we don't spit double tracebacks if we are raising + # SystemExit from the `except catch` block + + if exit_from_catch: + sys.exit(1) + + str_failure = traceback.format_exc() + for line in str_failure.split('\n'): + logger.error("%s" % line) + sys.exit(1) + + return newfunc + + return decorate + +# +# Decorator helpers +# + + +def make_exception_message(exc): + """ + An exception is passed in and this function + returns the proper string depending on the result + so it is readable enough. + """ + if str(exc): + return '%s: %s\n' % (exc.__class__.__name__, exc) + else: + return '%s\n' % (exc.__class__.__name__) + diff --git a/ceph_deploy/util/files.py b/ceph_deploy/util/files.py new file mode 100644 index 0000000..6770596 --- /dev/null +++ b/ceph_deploy/util/files.py @@ -0,0 +1,5 @@ + + +def read_file(path): + with open(path, 'rb') as f: + return f.read() diff --git a/ceph_deploy/util/help_formatters.py b/ceph_deploy/util/help_formatters.py new file mode 100644 index 0000000..2cb562d --- /dev/null +++ b/ceph_deploy/util/help_formatters.py @@ -0,0 +1,33 @@ +import argparse + + +class ToggleRawTextHelpFormatter(argparse.HelpFormatter): + """ArgParse help formatter that allows raw text in individual help strings + + Inspired by the SmartFormatter at + https://bitbucket.org/ruamel/std.argparse + + Normally to include newlines in the help output of argparse, you have + use argparse.RawDescriptionHelpFormatter. But this means raw text is enabled + everywhere, and not just for specific help entries where you might need it. + + This help formatter allows for you to optional enable/toggle raw text on + individual menu items by prefixing the help string with 'R|'. + + Example: + + parser.formatter_class = ToggleRawTextHelpFormatter + parser.add_argument('--verbose', action=store_true, + help='Enable verbose mode') + #Above help is formatted just as default argparse.HelpFormatter + + parser.add_argument('--complex-arg', action=store_true, + help=('R|This help description use ' + 'newlines and tabs and they will be preserved in' + 'the help output.\n\n' + '\tHow cool is that?')) + """ + def _split_lines(self, text, width): + if text.startswith('R|'): + return text[2:].splitlines() + return argparse.HelpFormatter._split_lines(self, text, width) diff --git a/ceph_deploy/util/log.py b/ceph_deploy/util/log.py new file mode 100644 index 0000000..c72303c --- /dev/null +++ b/ceph_deploy/util/log.py @@ -0,0 +1,67 @@ +import logging +import sys + +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + +COLORS = { + 'WARNING': YELLOW, + 'INFO': WHITE, + 'DEBUG': BLUE, + 'CRITICAL': RED, + 'ERROR': RED, + 'FATAL': RED, +} + +RESET_SEQ = "\033[0m" +COLOR_SEQ = "\033[1;%dm" +BOLD_SEQ = "\033[1m" + +BASE_COLOR_FORMAT = "[$BOLD%(name)s$RESET][%(color_levelname)-17s] %(message)s" +BASE_FORMAT = "[%(name)s][%(levelname)-6s] %(message)s" +FILE_FORMAT = "[%(asctime)s]" + BASE_FORMAT + +def supports_color(): + """ + Returns True if the running system's terminal supports color, and False + otherwise. + """ + unsupported_platform = (sys.platform in ('win32', 'Pocket PC')) + # isatty is not always implemented, #6223. + is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() + if unsupported_platform or not is_a_tty: + return False + return True + + +def color_message(message): + message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) + return message + + +class ColoredFormatter(logging.Formatter): + """ + A very basic logging formatter that not only applies color to the levels of + the ouput but will also truncate the level names so that they do not alter + the visuals of logging when presented on the terminal. + """ + + def __init__(self, msg): + logging.Formatter.__init__(self, msg) + + def format(self, record): + levelname = record.levelname + truncated_level = record.levelname[:6] + levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + truncated_level + RESET_SEQ + record.color_levelname = levelname_color + return logging.Formatter.format(self, record) + + +def color_format(): + """ + Main entry point to get a colored formatter, it will use the + BASE_FORMAT by default and fall back to no colors if the system + does not support it + """ + str_format = BASE_COLOR_FORMAT if supports_color() else BASE_FORMAT + color_format = color_message(str_format) + return ColoredFormatter(color_format) diff --git a/ceph_deploy/util/net.py b/ceph_deploy/util/net.py new file mode 100644 index 0000000..fe30efe --- /dev/null +++ b/ceph_deploy/util/net.py @@ -0,0 +1,399 @@ +try: + from urllib.request import urlopen + from urllib.error import HTTPError +except ImportError: + from urllib2 import urlopen, HTTPError + +from ceph_deploy import exc +import logging +import re +import socket +from ceph_deploy.lib import remoto + + +LOG = logging.getLogger(__name__) + + +# TODO: at some point, it might be way more accurate to do this in the actual +# host where we need to get IPs from. SaltStack does this by calling `ip` and +# parsing the output, which is probably the one true way of dealing with it. + +def get_nonlocal_ip(host, subnet=None): + """ + Search result of getaddrinfo() for a non-localhost-net address + """ + try: + ailist = socket.getaddrinfo(host, None) + except socket.gaierror: + raise exc.UnableToResolveError(host) + for ai in ailist: + # an ai is a 5-tuple; the last element is (ip, port) + ip = ai[4][0] + if subnet and ip_in_subnet(ip, subnet): + LOG.info('found ip (%s) for host (%s) to be in cluster subnet (%s)' % ( + ip, + host, + subnet,) + ) + + return ip + + if not ip.startswith('127.'): + if subnet: + LOG.warning('could not match ip (%s) for host (%s) for cluster subnet (%s)' % ( + ip, + host, + subnet,) + ) + return ip + raise exc.UnableToResolveError(host) + + +def ip_in_subnet(ip, subnet): + """Does IP exists in a given subnet utility. Returns a boolean""" + ipaddr = int(''.join(['%02x' % int(x) for x in ip.split('.')]), 16) + netstr, bits = subnet.split('/') + netaddr = int(''.join(['%02x' % int(x) for x in netstr.split('.')]), 16) + mask = (0xffffffff << (32 - int(bits))) & 0xffffffff + return (ipaddr & mask) == (netaddr & mask) + + +def in_subnet(cidr, addrs=None): + """ + Returns True if host is within specified subnet, otherwise False + """ + for address in addrs: + if ip_in_subnet(address, cidr): + return True + return False + + +def ip_addresses(conn, interface=None, include_loopback=False): + """ + Returns a list of IPv4/IPv6 addresses assigned to the host. 127.0.0.1/::1 is + ignored, unless 'include_loopback=True' is indicated. If 'interface' is + provided, then only IP addresses from that interface will be returned. + + Example output looks like:: + + >>> ip_addresses(conn) + >>> ['192.168.1.111', '10.0.1.12', '2001:db8::100'] + + """ + ret = set() + ifaces = linux_interfaces(conn) + if interface is None: + target_ifaces = ifaces + else: + target_ifaces = dict((k, v) for k, v in ifaces.items() + if k == interface) + if not target_ifaces: + LOG.error('Interface {0} not found.'.format(interface)) + for info in target_ifaces.values(): + for ipv4 in info.get('inet', []): + loopback = in_subnet('127.0.0.0/8', [ipv4.get('address')]) or ipv4.get('label') == 'lo' + if not loopback or include_loopback: + ret.add(ipv4['address']) + for secondary in info.get('secondary', []): + addr = secondary.get('address') + if addr and secondary.get('type') == 'inet': + if include_loopback or (not include_loopback and not in_subnet('127.0.0.0/8', [addr])): + ret.add(addr) + for ipv6 in info.get('inet6', []): + # When switching to Python 3 the IPAddress module can do all this work for us + if ipv6.get('address').startswith('fe80::'): + continue + + if not include_loopback and '::1' == ipv6.get('address'): + continue + + ret.add(ipv6['address']) + if ret: + conn.logger.debug('IP addresses found: %s' % str(list(ret))) + return sorted(list(ret)) + + +def linux_interfaces(conn): + """ + Obtain interface information for *NIX/BSD variants in remote servers. + + Example output from a remote node with a couple of interfaces:: + + {'eth0': {'hwaddr': '08:00:27:08:c2:e4', + 'inet': [{'address': '10.0.2.15', + 'broadcast': '10.0.2.255', + 'label': 'eth0', + 'netmask': '255.255.255.0'}], + 'inet6': [{'address': 'fe80::a00:27ff:fe08:c2e4', + 'prefixlen': '64'}], + 'up': True}, + 'eth1': {'hwaddr': '08:00:27:70:06:f1', + 'inet': [{'address': '192.168.111.101', + 'broadcast': '192.168.111.255', + 'label': 'eth1', + 'netmask': '255.255.255.0'}], + 'inet6': [{'address': 'fe80::a00:27ff:fe70:6f1', + 'prefixlen': '64'}], + 'up': True}, + 'lo': {'hwaddr': '00:00:00:00:00:00', + 'inet': [{'address': '127.0.0.1', + 'broadcast': None, + 'label': 'lo', + 'netmask': '255.0.0.0'}], + 'inet6': [{'address': '::1', 'prefixlen': '128'}], + 'up': True}} + + :param conn: A connection object to a remote node + """ + ifaces = dict() + ip_path = conn.remote_module.which('ip') + ifconfig_path = None if ip_path else conn.remote_module.which('ifconfig') + if ip_path: + cmd1, _, _ = remoto.process.check( + conn, + [ + '{0}'.format(ip_path), + 'link', + 'show', + ], + ) + cmd2, _, _ = remoto.process.check( + conn, + [ + '{0}'.format(ip_path), + 'addr', + 'show', + ], + ) + ifaces = _interfaces_ip('\n'.join(cmd1) + '\n' + + '\n'.join(cmd2)) + elif ifconfig_path: + cmd, _, _ = remoto.process.check( + conn, + [ + '{0}'.format(ifconfig_path), + '-a', + ] + ) + ifaces = _interfaces_ifconfig('\n'.join(cmd)) + return ifaces + + +def _interfaces_ip(out): + """ + Uses ip to return a dictionary of interfaces with various information about + each (up/down state, ip address, netmask, and hwaddr) + """ + ret = dict() + + def parse_network(value, cols): + """ + Return a tuple of ip, netmask, broadcast + based on the current set of cols + """ + brd = None + if '/' in value: # we have a CIDR in this address + ip, cidr = value.split('/') # pylint: disable=C0103 + else: + ip = value # pylint: disable=C0103 + cidr = 32 + + if type_ == 'inet': + mask = cidr_to_ipv4_netmask(int(cidr)) + if 'brd' in cols: + brd = cols[cols.index('brd') + 1] + elif type_ == 'inet6': + mask = cidr + return (ip, mask, brd) + + groups = re.compile('\r?\n\\d').split(out) + for group in groups: + iface = None + data = dict() + + for line in group.splitlines(): + if ' ' not in line: + continue + match = re.match(r'^\d*:\s+([\w.\-]+)(?:@)?([\w.\-]+)?:\s+<(.+)>', line) + if match: + iface, parent, attrs = match.groups() + if 'UP' in attrs.split(','): + data['up'] = True + else: + data['up'] = False + if parent: + data['parent'] = parent + continue + + cols = line.split() + if len(cols) >= 2: + type_, value = tuple(cols[0:2]) + iflabel = cols[-1:][0] + if type_ in ('inet', 'inet6'): + if 'secondary' not in cols: + ipaddr, netmask, broadcast = parse_network(value, cols) + if type_ == 'inet': + if 'inet' not in data: + data['inet'] = list() + addr_obj = dict() + addr_obj['address'] = ipaddr + addr_obj['netmask'] = netmask + addr_obj['broadcast'] = broadcast + addr_obj['label'] = iflabel + data['inet'].append(addr_obj) + elif type_ == 'inet6': + if 'inet6' not in data: + data['inet6'] = list() + addr_obj = dict() + addr_obj['address'] = ipaddr + addr_obj['prefixlen'] = netmask + data['inet6'].append(addr_obj) + else: + if 'secondary' not in data: + data['secondary'] = list() + ip_, mask, brd = parse_network(value, cols) + data['secondary'].append({ + 'type': type_, + 'address': ip_, + 'netmask': mask, + 'broadcast': brd, + 'label': iflabel, + }) + del ip_, mask, brd + elif type_.startswith('link'): + data['hwaddr'] = value + if iface: + ret[iface] = data + del iface, data + return ret + + +def _interfaces_ifconfig(out): + """ + Uses ifconfig to return a dictionary of interfaces with various information + about each (up/down state, ip address, netmask, and hwaddr) + """ + ret = dict() + + piface = re.compile(r'^([^\s:]+)') + pmac = re.compile('.*?(?:HWaddr|ether|address:|lladdr) ([0-9a-fA-F:]+)') + pip = re.compile(r'.*?(?:inet addr:|inet )(.*?)\s') + pip6 = re.compile('.*?(?:inet6 addr: (.*?)/|inet6 )([0-9a-fA-F:]+)') + pmask = re.compile(r'.*?(?:Mask:|netmask )(?:((?:0x)?[0-9a-fA-F]{8})|([\d\.]+))') + pmask6 = re.compile(r'.*?(?:inet6 addr: [0-9a-fA-F:]+/(\d+)|prefixlen (\d+)).*') + pupdown = re.compile('UP') + pbcast = re.compile(r'.*?(?:Bcast:|broadcast )([\d\.]+)') + + groups = re.compile('\r?\n(?=\\S)').split(out) + for group in groups: + data = dict() + iface = '' + updown = False + for line in group.splitlines(): + miface = piface.match(line) + mmac = pmac.match(line) + mip = pip.match(line) + mip6 = pip6.match(line) + mupdown = pupdown.search(line) + if miface: + iface = miface.group(1) + if mmac: + data['hwaddr'] = mmac.group(1) + if mip: + if 'inet' not in data: + data['inet'] = list() + addr_obj = dict() + addr_obj['address'] = mip.group(1) + mmask = pmask.match(line) + if mmask: + if mmask.group(1): + mmask = _number_of_set_bits_to_ipv4_netmask( + int(mmask.group(1), 16)) + else: + mmask = mmask.group(2) + addr_obj['netmask'] = mmask + mbcast = pbcast.match(line) + if mbcast: + addr_obj['broadcast'] = mbcast.group(1) + data['inet'].append(addr_obj) + if mupdown: + updown = True + if mip6: + if 'inet6' not in data: + data['inet6'] = list() + addr_obj = dict() + addr_obj['address'] = mip6.group(1) or mip6.group(2) + mmask6 = pmask6.match(line) + if mmask6: + addr_obj['prefixlen'] = mmask6.group(1) or mmask6.group(2) + data['inet6'].append(addr_obj) + data['up'] = updown + ret[iface] = data + del data + return ret + + +def _number_of_set_bits_to_ipv4_netmask(set_bits): # pylint: disable=C0103 + """ + Returns an IPv4 netmask from the integer representation of that mask. + + Ex. 0xffffff00 -> '255.255.255.0' + """ + return cidr_to_ipv4_netmask(_number_of_set_bits(set_bits)) + + +def _number_of_set_bits(x): + """ + Returns the number of bits that are set in a 32bit int + """ + # Taken from http://stackoverflow.com/a/4912729. Many thanks! + x -= (x >> 1) & 0x55555555 + x = ((x >> 2) & 0x33333333) + (x & 0x33333333) + x = ((x >> 4) + x) & 0x0f0f0f0f + x += x >> 8 + x += x >> 16 + return x & 0x0000003f + + +def cidr_to_ipv4_netmask(cidr_bits): + """ + Returns an IPv4 netmask + """ + try: + cidr_bits = int(cidr_bits) + if not 1 <= cidr_bits <= 32: + return '' + except ValueError: + return '' + + netmask = '' + for idx in range(4): + if idx: + netmask += '.' + if cidr_bits >= 8: + netmask += '255' + cidr_bits -= 8 + else: + netmask += '{0:d}'.format(256 - (2 ** (8 - cidr_bits))) + cidr_bits = 0 + return netmask + + +def get_request(url): + try: + return urlopen(url) + except HTTPError as err: + LOG.error('repository might not be available yet') + raise RuntimeError('%s, failed to fetch %s' % (err, url)) + + +def get_chacra_repo(shaman_url): + """ + From a Shaman URL, get the chacra url for a repository, read the + contents that point to the repo and return it as a string. + """ + shaman_response = get_request(shaman_url) + chacra_url = shaman_response.geturl() + chacra_response = get_request(chacra_url) + + return chacra_response.read() diff --git a/ceph_deploy/util/packages.py b/ceph_deploy/util/packages.py new file mode 100644 index 0000000..a998264 --- /dev/null +++ b/ceph_deploy/util/packages.py @@ -0,0 +1,74 @@ +from ceph_deploy.exc import ExecutableNotFound +from ceph_deploy.util import system, versions +from ceph_deploy.lib import remoto + + +class Ceph(object): + """ + Determine different aspects of the Ceph package, like ``version`` and path + ``executable``. Although mostly provide a version object that helps for + parsing and comparing. + """ + + def __init__(self, conn, _check=None): + self.conn = conn + self._check = _check or remoto.process.check + + @property + def installed(self): + """ + If the ``ceph`` executable exists, then Ceph is installed. Should + probably be revisited if different components do not have the ``ceph`` + executable (this is currently provided by ``ceph-common``). + """ + return bool(self.executable) + + @property + def executable(self): + try: + return system.executable_path(self.conn, 'ceph') + except ExecutableNotFound: + return None + + def _get_version_output(self): + """ + Ignoring errors, call `ceph --version` and return only the version + portion of the output. For example, output like:: + + ceph version 9.0.1-1234kjd (asdflkj2k3jh234jhg) + + Would return:: + + 9.0.1-1234kjd + """ + if not self.executable: + return '' + command = [self.executable, '--version'] + out, _, _ = self._check(self.conn, command) + try: + return out.decode('utf-8').split()[2] + except IndexError: + return '' + + @property + def version(self): + """ + Return a version object (see + :mod:``ceph_deploy.util.versions.NormalizedVersion``) + """ + return versions.parse_version(self._get_version_output) + + +# callback helpers + +def ceph_is_installed(module): + """ + A helper callback to be executed after the connection is made to ensure + that Ceph is installed. + """ + ceph_package = Ceph(module.conn) + if not ceph_package.installed: + host = module.conn.hostname + raise RuntimeError( + 'ceph needs to be installed in remote host: %s' % host + ) diff --git a/ceph_deploy/util/paths/__init__.py b/ceph_deploy/util/paths/__init__.py new file mode 100644 index 0000000..287a551 --- /dev/null +++ b/ceph_deploy/util/paths/__init__.py @@ -0,0 +1,3 @@ +from . import mon # noqa +from . import osd # noqa +from . import gpg # noqa diff --git a/ceph_deploy/util/paths/gpg.py b/ceph_deploy/util/paths/gpg.py new file mode 100644 index 0000000..d9d950b --- /dev/null +++ b/ceph_deploy/util/paths/gpg.py @@ -0,0 +1,8 @@ +from ceph_deploy.util import constants + +def url(key_type, protocol="https"): + return "{protocol}://{url}{key_type}.asc".format( + protocol=protocol, + url=constants.gpg_key_base_url, + key_type=key_type + ) diff --git a/ceph_deploy/util/paths/mon.py b/ceph_deploy/util/paths/mon.py new file mode 100644 index 0000000..0c252d5 --- /dev/null +++ b/ceph_deploy/util/paths/mon.py @@ -0,0 +1,84 @@ +""" +Common paths for mon, based on the constant file paths defined in +``ceph_deploy.util.constants``. +All functions return a string representation of the absolute path +construction. +""" +from os.path import join + +from ceph_deploy.util import constants + + +def base(cluster): + cluster = "%s-" % cluster + return join(constants.mon_path, cluster) + + +def path(cluster, hostname): + """ + Example usage:: + + >>> from ceph_deploy.util.paths import mon + >>> mon.path('mycluster', 'hostname') + /var/lib/ceph/mon/mycluster-myhostname + """ + return "%s%s" % (base(cluster), hostname) + + +def done(cluster, hostname): + """ + Example usage:: + + >>> from ceph_deploy.util.paths import mon + >>> mon.done('mycluster', 'hostname') + /var/lib/ceph/mon/mycluster-myhostname/done + """ + return join(path(cluster, hostname), 'done') + + +def init(cluster, hostname, init): + """ + Example usage:: + + >>> from ceph_deploy.util.paths import mon + >>> mon.init('mycluster', 'hostname', 'init') + /var/lib/ceph/mon/mycluster-myhostname/init + """ + return join(path(cluster, hostname), init) + + +def keyring(cluster, hostname): + """ + Example usage:: + + >>> from ceph_deploy.util.paths import mon + >>> mon.keyring('mycluster', 'myhostname') + /var/lib/ceph/tmp/mycluster-myhostname.mon.keyring + """ + keyring_file = '%s-%s.mon.keyring' % (cluster, hostname) + return join(constants.tmp_path, keyring_file) + + +def asok(cluster, hostname): + """ + Example usage:: + + >>> from ceph_deploy.util.paths import mon + >>> mon.asok('mycluster', 'myhostname') + /var/run/ceph/mycluster-mon.myhostname.asok + """ + asok_file = '%s-mon.%s.asok' % (cluster, hostname) + return join(constants.base_run_path, asok_file) + + +def monmap(cluster, hostname): + """ + Example usage:: + + >>> from ceph_deploy.util.paths import mon + >>> mon.monmap('mycluster', 'myhostname') + /var/lib/ceph/tmp/mycluster.myhostname.monmap + """ + monmap + mon_map_file = '%s.%s.monmap' % (cluster, hostname) + return join(constants.tmp_path, mon_map_file) diff --git a/ceph_deploy/util/paths/osd.py b/ceph_deploy/util/paths/osd.py new file mode 100644 index 0000000..18d7502 --- /dev/null +++ b/ceph_deploy/util/paths/osd.py @@ -0,0 +1,13 @@ +""" +Comosd paths for osd, based on the constant file paths defined in +``ceph_deploy.util.constants``. +All functions return a string representation of the absolute path +construction. +""" +from os.path import join +from ceph_deploy.util import constants + + +def base(cluster): + cluster = "%s-" % cluster + return join(constants.osd_path, cluster) diff --git a/ceph_deploy/util/pkg_managers.py b/ceph_deploy/util/pkg_managers.py new file mode 100644 index 0000000..d05db5d --- /dev/null +++ b/ceph_deploy/util/pkg_managers.py @@ -0,0 +1,479 @@ +import os +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + +from ceph_deploy.lib import remoto +from ceph_deploy.util import templates + + +class PackageManager(object): + """ + Base class for all Package Managers + """ + + def __init__(self, remote_conn): + self.remote_info = remote_conn + self.remote_conn = remote_conn.conn + + def _run(self, cmd, **kw): + return remoto.process.run( + self.remote_conn, + cmd, + **kw + ) + + def _check(self, cmd, **kw): + return remoto.process.check( + self.remote_conn, + cmd, + **kw + ) + + def install(self, packages, **kw): + """Install packages on remote node""" + raise NotImplementedError() + + def remove(self, packages, **kw): + """Uninstall packages on remote node""" + raise NotImplementedError() + + def clean(self): + """Clean metadata/cache""" + raise NotImplementedError() + + def add_repo_gpg_key(self, url): + """Add given GPG key for repo verification""" + raise NotImplementedError() + + def add_repo(self, name, url, **kw): + """Add/rewrite a repo file""" + raise NotImplementedError() + + def remove_repo(self, name): + """Remove a repo definition""" + raise NotImplementedError() + + +class RPMManagerBase(PackageManager): + """ + Base class to hold common pieces of Yum and DNF + """ + + executable = None + name = None + + def install(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_install_flags', None) + cmd = [ + self.executable, + '-y', + 'install', + ] + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + + cmd.extend(packages) + return self._run(cmd) + + def remove(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_remove_flags', None) + cmd = [ + self.executable, + '-y', + '-q', + 'remove', + ] + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + cmd.extend(packages) + return self._run(cmd) + + def clean(self, item=None): + item = item or 'all' + cmd = [ + self.executable, + 'clean', + item, + ] + + return self._run(cmd) + + def add_repo_gpg_key(self, url): + cmd = ['rpm', '--import', url] + self._run(cmd) + + def add_repo(self, name, url, **kw): + gpg_url = kw.pop('gpg_url', None) + if gpg_url: + self.add_repo_gpg_key(gpg_url) + gpgcheck=1 + else: + gpgcheck=0 + + # RPM repo defaults + description = kw.pop('description', '%s repo' % name) + enabled = kw.pop('enabled', 1) + proxy = kw.pop('proxy', '') # will get ignored if empty + _type = 'repo-md' + baseurl = url.strip('/') # Remove trailing slashes + + ceph_repo_content = templates.custom_repo( + reponame=name, + name=description, + baseurl=baseurl, + enabled=enabled, + gpgcheck=gpgcheck, + _type=_type, + gpgkey=gpg_url, + proxy=proxy, + **kw + ) + + self.remote_conn.remote_module.write_yum_repo( + ceph_repo_content, + '%s.repo' % name + ) + + def remove_repo(self, name): + filename = os.path.join( + '/etc/yum.repos.d', + '%s.repo' % name + ) + self.remote_conn.remote_module.unlink(filename) + + +class DNF(RPMManagerBase): + """ + The DNF Package manager + """ + + executable = 'dnf' + name = 'dnf' + + def install(self, packages, **kw): + extra_install_flags = kw.pop('extra_install_flags', []) + if '--best' not in extra_install_flags: + extra_install_flags.append('--best') + super(DNF, self).install( + packages, + extra_install_flags=extra_install_flags, + **kw + ) + + +class Yum(RPMManagerBase): + """ + The Yum Package manager + """ + + executable = 'yum' + name = 'yum' + + +class Apt(PackageManager): + """ + Apt package management + """ + + executable = [ + 'env', + 'DEBIAN_FRONTEND=noninteractive', + 'DEBIAN_PRIORITY=critical', + 'apt-get', + '--assume-yes', + '-q', + ] + name = 'apt' + + def install(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_install_flags', None) + cmd = self.executable + [ + '--no-install-recommends', + 'install' + ] + + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + cmd.extend(packages) + return self._run(cmd) + + def remove(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_remove_flags', None) + cmd = self.executable + [ + '-f', + '--force-yes', + 'remove' + ] + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + + cmd.extend(packages) + return self._run(cmd) + + def clean(self): + cmd = self.executable + ['update'] + return self._run(cmd) + + def add_repo_gpg_key(self, url): + gpg_path = url.split('file://')[-1] + if not url.startswith('file://'): + cmd = ['wget', '-O', 'release.asc', url ] + self._run(cmd, stop_on_nonzero=False) + gpg_file = 'release.asc' if not url.startswith('file://') else gpg_path + cmd = ['apt-key', 'add', gpg_file] + self._run(cmd) + + def add_repo(self, name, url, **kw): + gpg_url = kw.pop('gpg_url', None) + if gpg_url: + self.add_repo_gpg_key(gpg_url) + + safe_filename = '%s.list' % name.replace(' ', '-') + mode = 0o644 + if urlparse(url).password: + mode = 0o600 + self.remote_conn.logger.info( + "Creating repo file with mode 0600 due to presence of password" + ) + self.remote_conn.remote_module.write_sources_list( + url, + self.remote_info.codename, + safe_filename, + mode + ) + + # Add package pinning for this repo + fqdn = urlparse(url).hostname + self.remote_conn.remote_module.set_apt_priority(fqdn) + + def remove_repo(self, name): + safe_filename = '%s.list' % name.replace(' ', '-') + filename = os.path.join( + '/etc/apt/sources.list.d', + safe_filename + ) + self.remote_conn.remote_module.unlink(filename) + + +class Zypper(PackageManager): + """ + Zypper package management + """ + + executable = [ + 'zypper', + '--non-interactive', + '--quiet' + ] + name = 'zypper' + + def install(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_install_flags', None) + cmd = self.executable + ['install'] + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + cmd.extend(packages) + return self._run(cmd) + + def remove(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_remove_flags', None) + cmd = self.executable + ['--ignore-unknown', 'remove'] + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + cmd.extend(packages) + stdout, stderr, exitrc = self._check( + cmd, + **kw + ) + # exitrc is 104 when package(s) not installed. + if not exitrc in [0, 104]: + raise RuntimeError("Failed to execute command: %s" % " ".join(cmd)) + return + + def clean(self): + cmd = self.executable + ['refresh'] + return self._run(cmd) + + +class Pacman(PackageManager): + """ + Pacman package management + """ + + executable = [ + 'pacman', + '--noconfirm', + ] + name = 'pacman' + + def install(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_install_flags', None) + cmd = self.executable + [ + '-Sy', + ] + + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + cmd.extend(packages) + return self._run(cmd) + + def remove(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_remove_flags', None) + cmd = self.executable + [ + '-R' + ] + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + + cmd.extend(packages) + return self._run(cmd) + + def clean(self): + cmd = self.executable + ['-Syy'] + return self._run(cmd) + + def add_repo_gpg_key(self, url): + cmd = ['pacman-key', '-a', url] + self._run(cmd) + + +class AptRpm(PackageManager): + """ + Apt-Rpm package management + """ + + executable = [ + 'apt-get', + '-y', + '-q', + '-V', + ] + name = 'apt' + + def install(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_install_flags', None) + cmd = self.executable + ['install'] + + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + cmd.extend(packages) + return self._run(cmd) + + def remove(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + + extra_flags = kw.pop('extra_remove_flags', None) + cmd = self.executable + [ + '-y', + 'remove' + ] + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + + cmd.extend(packages) + return self._run(cmd) + + def clean(self): + cmd = self.executable + ['update'] + return self._run(cmd) + + +class Swupd(PackageManager): + """ + Swupd package manager from ClearLinux distribution + """ + + executable = [ + 'swupd', + ] + name = 'swupd' + + def install(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_install_flags', None) + # Prior to installing packages, make sure we create folders for ceph + # config and logging which do not exist in Clear + cmd = ['mkdir', '-p', '/etc/ceph/'] + self._run(cmd) + cmd = ['mkdir', '-p', '/var/log/ceph/'] + self._run(cmd) + + cmd = self.executable + ['bundle-add'] + + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + cmd.extend(packages) + return self._run(cmd) + + def remove(self, packages, **kw): + if isinstance(packages, str): + packages = [packages] + + extra_flags = kw.pop('extra_remove_flags', None) + cmd = self.executable + ['bundle-remove'] + + if extra_flags: + if isinstance(extra_flags, str): + extra_flags = [extra_flags] + cmd.extend(extra_flags) + cmd.extend(packages) + return self._run(cmd) + + def clean(self): + cmd = self.executable + ['clean'] + return self._run(cmd) diff --git a/ceph_deploy/util/ssh.py b/ceph_deploy/util/ssh.py new file mode 100644 index 0000000..5576371 --- /dev/null +++ b/ceph_deploy/util/ssh.py @@ -0,0 +1,32 @@ +import logging +from ceph_deploy.lib import remoto +from ceph_deploy.connection import get_local_connection + + +def can_connect_passwordless(hostname): + """ + Ensure that current host can SSH remotely to the remote + host using the ``BatchMode`` option to prevent a password prompt. + + That attempt will error with an exit status of 255 and a ``Permission + denied`` message or a``Host key verification failed`` message. + """ + # Ensure we are not doing this for local hosts + if not remoto.backends.needs_ssh(hostname): + return True + + logger = logging.getLogger(hostname) + with get_local_connection(logger) as conn: + # Check to see if we can login, disabling password prompts + command = ['ssh', '-CT', '-o', 'BatchMode=yes', hostname, 'true'] + out, err, retval = remoto.process.check(conn, command, stop_on_error=False) + permission_denied_error = 'Permission denied ' + host_key_verify_error = 'Host key verification failed.' + has_key_error = False + for line in err: + if permission_denied_error in line or host_key_verify_error in line: + has_key_error = True + + if retval == 255 and has_key_error: + return False + return True diff --git a/ceph_deploy/util/system.py b/ceph_deploy/util/system.py new file mode 100644 index 0000000..cff1305 --- /dev/null +++ b/ceph_deploy/util/system.py @@ -0,0 +1,180 @@ +from ceph_deploy.exc import ExecutableNotFound +from ceph_deploy.lib import remoto + + +def executable_path(conn, executable): + """ + Remote validator that accepts a connection object to ensure that a certain + executable is available returning its full path if so. + + Otherwise an exception with thorough details will be raised, informing the + user that the executable was not found. + """ + executable_path = conn.remote_module.which(executable) + if not executable_path: + raise ExecutableNotFound(executable, conn.hostname) + return executable_path + + +def is_systemd(conn): + """ + Attempt to detect if a remote system is a systemd one or not + by looking into ``/proc`` just like the ceph init script does:: + + # detect systemd + # SYSTEMD=0 + grep -qs systemd /proc/1/comm && SYSTEMD=1 + """ + return conn.remote_module.grep( + 'systemd', + '/proc/1/comm' + ) + + +def is_upstart(conn): + """ + This helper should only used as a fallback (last resort) as it is not + guaranteed that it will be absolutely correct. + """ + # it may be possible that we may be systemd and the caller never checked + # before so lets do that + if is_systemd(conn): + return False + + # get the initctl executable, if it doesn't exist we can't proceed so we + # are probably not upstart + initctl = conn.remote_module.which('initctl') + if not initctl: + return False + + # finally, try and get output from initctl that might hint this is an upstart + # system. On a Ubuntu 14.04.2 system this would look like: + # $ initctl version + # init (upstart 1.12.1) + stdout, stderr, _ = remoto.process.check( + conn, + [initctl, 'version'], + ) + result_string = b' '.join(stdout) + if b'upstart' in result_string: + return True + return False + + +def enable_service(conn, service='ceph'): + """ + Enable a service on a remote host depending on the type of init system. + Obviously, this should be done for RHEL/Fedora/CentOS systems. + + This function does not do any kind of detection. + """ + if is_systemd(conn): + remoto.process.run( + conn, + [ + 'systemctl', + 'enable', + '{service}'.format(service=service), + ] + ) + else: + remoto.process.run( + conn, + [ + 'chkconfig', + '{service}'.format(service=service), + 'on', + ] + ) + + +def disable_service(conn, service='ceph'): + """ + Disable a service on a remote host depending on the type of init system. + Obviously, this should be done for RHEL/Fedora/CentOS systems. + + This function does not do any kind of detection. + """ + if is_systemd(conn): + # Without the check, an error is raised trying to disable an + # already disabled service + if is_systemd_service_enabled(conn, service): + remoto.process.run( + conn, + [ + 'systemctl', + 'disable', + '{service}'.format(service=service), + ] + ) + + +def stop_service(conn, service='ceph'): + """ + Stop a service on a remote host depending on the type of init system. + Obviously, this should be done for RHEL/Fedora/CentOS systems. + + This function does not do any kind of detection. + """ + if is_systemd(conn): + # Without the check, an error is raised trying to stop an + # already stopped service + if is_systemd_service_active(conn, service): + remoto.process.run( + conn, + [ + 'systemctl', + 'stop', + '{service}'.format(service=service), + ] + ) + + +def start_service(conn, service='ceph'): + """ + Stop a service on a remote host depending on the type of init system. + Obviously, this should be done for RHEL/Fedora/CentOS systems. + + This function does not do any kind of detection. + """ + if is_systemd(conn): + remoto.process.run( + conn, + [ + 'systemctl', + 'start', + '{service}'.format(service=service), + ] + ) + + +def is_systemd_service_active(conn, service='ceph'): + """ + Detects if a systemd service is active or not. + """ + _, _, returncode = remoto.process.check( + conn, + [ + 'systemctl', + 'is-active', + '--quiet', + '{service}'.format(service=service), + ] + ) + return returncode == 0 + + +def is_systemd_service_enabled(conn, service='ceph'): + """ + Detects if a systemd service is enabled or not. + """ + _, _, returncode = remoto.process.check( + conn, + [ + 'systemctl', + 'is-enabled', + '--quiet', + '{service}'.format(service=service), + ] + ) + return returncode == 0 diff --git a/ceph_deploy/util/templates.py b/ceph_deploy/util/templates.py new file mode 100644 index 0000000..b54f7ac --- /dev/null +++ b/ceph_deploy/util/templates.py @@ -0,0 +1,94 @@ + + +ceph_repo = """[ceph] +name=Ceph packages for $basearch +baseurl={repo_url}/$basearch +enabled=1 +gpgcheck={gpgcheck} +priority=1 +type=rpm-md +gpgkey={gpg_url} + +[ceph-noarch] +name=Ceph noarch packages +baseurl={repo_url}/noarch +enabled=1 +gpgcheck={gpgcheck} +priority=1 +type=rpm-md +gpgkey={gpg_url} + +[ceph-source] +name=Ceph source packages +baseurl={repo_url}/SRPMS +enabled=0 +gpgcheck={gpgcheck} +type=rpm-md +gpgkey={gpg_url} +""" + +zypper_repo = """[ceph] +name=Ceph packages +type=rpm-md +baseurl={repo_url} +gpgcheck={gpgcheck} +gpgkey={gpg_url} +enabled=1 +""" + + +def custom_repo(**kw): + """ + Repo files need special care in that a whole line should not be present + if there is no value for it. Because we were using `format()` we could + not conditionally add a line for a repo file. So the end result would + contain a key with a missing value (say if we were passing `None`). + + For example, it could look like:: + + [ceph repo] + name= ceph repo + proxy= + gpgcheck= + + Which breaks. This function allows us to conditionally add lines, + preserving an order and be more careful. + + Previously, and for historical purposes, this is how the template used + to look:: + + custom_repo = + [{repo_name}] + name={name} + baseurl={baseurl} + enabled={enabled} + gpgcheck={gpgcheck} + type={_type} + gpgkey={gpgkey} + proxy={proxy} + + """ + lines = [] + + # by using tuples (vs a dict) we preserve the order of what we want to + # return, like starting with a [repo name] + tmpl = ( + ('reponame', '[%s]'), + ('name', 'name=%s'), + ('baseurl', 'baseurl=%s'), + ('enabled', 'enabled=%s'), + ('gpgcheck', 'gpgcheck=%s'), + ('_type', 'type=%s'), + ('gpgkey', 'gpgkey=%s'), + ('proxy', 'proxy=%s'), + ('priority', 'priority=%s'), + ) + + for line in tmpl: + tmpl_key, tmpl_value = line # key values from tmpl + + # ensure that there is an actual value (not None nor empty string) + if tmpl_key in kw and kw.get(tmpl_key) not in (None, ''): + lines.append(tmpl_value % kw.get(tmpl_key)) + + return '\n'.join(lines) diff --git a/ceph_deploy/util/versions.py b/ceph_deploy/util/versions.py new file mode 100644 index 0000000..810eb38 --- /dev/null +++ b/ceph_deploy/util/versions.py @@ -0,0 +1,47 @@ + + +class NormalizedVersion(object): + """ + A class to provide a clean interface for setting/retrieving distinct + version parts divided into major, minor, and patch (following convnetions + from semver (see http://semver.org/) + + Since a lot of times version parts need to be compared, it provides for + `int` representations of their string counterparts, with some sanitization + processing. + + Defaults to '0' or 0 (int) values when values are not set or parsing fails. + """ + + def __init__(self, raw_version): + self.raw_version = raw_version.strip() + self.major = '0' + self.minor = '0' + self.patch = '0' + self.garbage = '' + self.int_major = 0 + self.int_minor = 0 + self.int_patch = 0 + self._version_map = {} + self._set_versions() + + def _set_int_versions(self): + version_map = dict( + major=self.major, + minor=self.minor, + patch=self.patch, + garbage=self.garbage) + + # safe int versions that remove non-numerical chars + # for example 'rc1' in a version like '1-rc1 + for name, value in version_map.items(): + if '-' in value: # get rid of garbage like -dev1 or -rc1 + value = value.split('-')[0] + value = float(''.join(c for c in value if c.isdigit()) or 0) + int_name = "int_%s" % name + setattr(self, int_name, value) + + def _set_versions(self): + split_version = (self.raw_version.split('.') + ["0"]*4)[:4] + self.major, self.minor, self.patch, self.garbage = split_version + self._set_int_versions() diff --git a/ceph_deploy/validate.py b/ceph_deploy/validate.py new file mode 100644 index 0000000..8ef5e73 --- /dev/null +++ b/ceph_deploy/validate.py @@ -0,0 +1,16 @@ +import argparse +import re + + +ALPHANUMERIC_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9]*$') + + +def alphanumeric(s): + """ + Enforces string to be alphanumeric with leading alpha. + """ + if not ALPHANUMERIC_RE.match(s): + raise argparse.ArgumentTypeError( + 'argument must start with a letter and contain only letters and numbers', + ) + return s diff --git a/debian/ceph-deploy.install b/debian/ceph-deploy.install new file mode 100644 index 0000000..cec4ab6 --- /dev/null +++ b/debian/ceph-deploy.install @@ -0,0 +1 @@ +./scripts/ceph-deploy /usr/bin diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..f947980 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,377 @@ +ceph-deploy (2.1.0) stable; urgency=medium + + * New upstream release + + -- Ceph Release Team Thu, 01 Oct 2020 17:52:03 +0000 + +ceph-deploy (2.0.1) stable; urgency=medium + + * New upstream release + + -- Ceph Release Team Tue, 19 Jun 2018 17:54:36 +0000 + +ceph-deploy (2.0.0) stable; urgency=medium + + * New upstream release + + -- Ceph Release Team Wed, 17 Jan 2018 13:17:46 +0000 + +ceph-deploy (1.5.39) stable; urgency=medium + + * New upstream release + + -- Ceph Release Team Fri, 01 Sep 2017 11:45:54 +0000 + +ceph-deploy (1.5.38) stable; urgency=medium + + * New upstream release + + -- Ceph Release Team Thu, 25 May 2017 12:35:46 +0000 + +ceph-deploy (1.5.37) stable; urgency=medium + + * New upstream release + + -- Alfredo Deza Tue, 03 Jan 2017 21:19:14 +0000 + +ceph-deploy (1.5.36) stable; urgency=medium + + * New upstream release + + -- Alfredo Deza Tue, 30 Aug 2016 11:47:41 +0000 + +ceph-deploy (1.5.35) stable; urgency=medium + + * New upstream release + + -- Alfredo Deza Mon, 15 Aug 2016 13:15:02 +0000 + +ceph-deploy (1.5.34) stable; urgency=medium + + * New upstream release + + -- Alfredo Deza Tue, 07 Jun 2016 17:06:26 +0000 + +ceph-deploy (1.5.33) stable; urgency=medium + + * New upstream release + + -- Alfredo Deza Fri, 22 Apr 2016 12:36:09 +0000 + +ceph-deploy (1.5.32) stable; urgency=medium + + * New upstream release + + -- Alfredo Deza Wed, 13 Apr 2016 14:21:57 +0000 + +ceph-deploy (1.5.31) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Mon, 04 Jan 2016 18:46:26 +0000 + +ceph-deploy (1.5.30) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Fri, 11 Dec 2015 21:09:05 +0000 + +ceph-deploy (1.5.29) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Wed, 02 Dec 2015 18:21:15 +0000 + +ceph-deploy (1.5.28) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Wed, 26 Aug 2015 11:25:15 -0700 + +ceph-deploy (1.5.27) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Wed, 05 Aug 2015 15:51:53 -0700 + +ceph-deploy (1.5.26) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Mon, 20 Jul 2015 11:09:38 -0700 + +ceph-deploy (1.5.25) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Tue, 26 May 2015 10:38:53 -0700 + +ceph-deploy (1.5.24) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Mon, 18 May 2015 13:35:00 -0700 + +ceph-deploy (1.5.23) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Tue, 07 Apr 2015 17:06:35 -0700 + +ceph-deploy (1.5.22) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Mon, 09 Mar 2015 08:14:20 -0700 + +ceph-deploy (1.5.21) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Wed, 10 Dec 2014 07:05:42 -0800 + +ceph-deploy (1.5.20) stable; urgency=low + + * New upstream release + + -- Travis Rhoden Thu, 13 Nov 2014 08:08:46 -0800 + +ceph-deploy (1.5.19) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Wed, 29 Oct 2014 07:19:41 -0700 + +ceph-deploy (1.5.18) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Thu, 09 Oct 2014 10:37:06 -0700 + +ceph-deploy (1.5.18) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Thu, 09 Oct 2014 09:38:44 -0700 + +ceph-deploy (1.5.17) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Mon, 06 Oct 2014 09:15:34 -0700 + +ceph-deploy (1.5.16) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Tue, 30 Sep 2014 07:25:13 -0700 + +ceph-deploy (1.5.15) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Fri, 12 Sep 2014 12:30:50 -0700 + +ceph-deploy (1.5.14) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Tue, 09 Sep 2014 13:51:23 -0700 + +ceph-deploy (1.5.13) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Wed, 03 Sep 2014 05:38:57 -0700 + +ceph-deploy (1.5.12) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Mon, 25 Aug 2014 13:04:17 -0700 + +ceph-deploy (1.5.12) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Mon, 25 Aug 2014 12:40:48 -0700 + +ceph-deploy (1.5.11) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Wed, 13 Aug 2014 05:29:28 -0700 + +ceph-deploy (1.5.10) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Thu, 31 Jul 2014 10:45:11 -0700 + +ceph-deploy (1.5.9) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Mon, 14 Jul 2014 10:12:18 -0700 + +ceph-deploy (1.5.8) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Wed, 09 Jul 2014 15:51:46 +0000 + +ceph-deploy (1.5.7) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Tue, 01 Jul 2014 20:54:52 +0000 + +ceph-deploy (1.5.6) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Tue, 01 Jul 2014 15:22:02 +0000 + +ceph-deploy (1.5.5) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Tue, 10 Jun 2014 14:12:23 +0000 + +ceph-deploy (1.5.4) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Fri, 06 Jun 2014 15:45:10 +0000 + +ceph-deploy (1.5.3) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Fri, 30 May 2014 12:56:14 +0000 + +ceph-deploy (1.5.2) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Wed, 07 May 2014 18:09:23 +0000 + +ceph-deploy (1.5.1) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Thu, 01 May 2014 16:09:56 +0000 + +ceph-deploy (1.5.0) stable; urgency=low + + * New upstream release + + -- Alfredo Deza Fri, 25 Apr 2014 20:15:18 +0000 + +ceph-deploy (1.4.0-1) UNRELEASED; urgency=low + + * New upstream release + + -- Alfredo Deza Wed, 19 Mar 2014 14:32:28 +0000 + +ceph-deploy (1.3.5-1) stable; urgency=low + + * New upstream release + + -- Ken Dreyer Wed, 05 Feb 2014 19:56:18 +0000 + +ceph-deploy (1.3.4-1) precise; urgency=low + + * New upstream release + + -- Gary Lowell Thu, 02 Jan 2014 17:01:21 -0800 + +ceph-deploy (1.3.3-1) stable; urgency=low + + * New upstream release + + -- Gary Lowell Tue, 26 Nov 2013 19:21:04 +0000 + +ceph-deploy (1.3.2-1) stable; urgency=low + + * New upstream release + + -- Gary Lowell Wed, 13 Nov 2013 00:22:12 +0000 + +ceph-deploy (1.3.1-1) stable; urgency=low + + * New upstream release + + -- Gary Lowell Wed, 06 Nov 2013 20:02:54 +0000 + +ceph-deploy (1.3-1) stable; urgency=low + + * New upstream release + + -- Gary Lowell Fri, 01 Nov 2013 05:28:02 +0000 + +ceph-deploy (1.2.7) stable; urgency=low + + * New upstream release + + -- Gary Lowell Mon, 07 Oct 2013 18:33:45 +0000 + +ceph-deploy (1.2.6-1) precise; urgency=low + + * New upstream release + + -- Gary Lowell Wed, 18 Sep 2013 09:26:57 -0700 + +ceph-deploy (1.2.5-1) precise; urgency=low + + * New upstream release + + -- Gary Lowell Tue, 17 Sep 2013 19:25:43 -0700 + +ceph-deploy (1.2.4-1) precise; urgency=low + + * New upstream release + + -- Gary Lowell Tue, 17 Sep 2013 11:19:59 -0700 + +ceph-deploy (1.2.3) precise; urgency=low + + * New upstream release + + -- Gary Lowell Thu, 29 Aug 2013 15:20:22 -0700 + +ceph-deploy (1.2.2) precise; urgency=low + + * New upstream release + + -- Gary Lowell Thu, 22 Aug 2013 12:26:56 -0700 + +ceph-deploy (1.2.1-1) precise; urgency=low + + * New upstream release + + -- Gary Lowell Thu, 15 Aug 2013 15:19:33 -0700 + +ceph-deploy (1.2-1) precise; urgency=low + + * New upstream release + + -- Gary Lowell Mon, 12 Aug 2013 16:59:09 -0700 + +ceph-deploy (1.1-1) precise; urgency=low + + * New upstream release + + -- Gary Lowell Tue, 18 Jun 2013 11:07:00 -0700 + +ceph-deploy (1.0-1) stable; urgency=low + + * New upstream release + + -- Gary Lowell Fri, 24 May 2013 11:57:40 +0800 + +ceph-deploy (0.0.1-1) unstable; urgency=low + + * Initial release. + + -- Gary Lowell Mon, 10 Mar 2013 18:38:40 +0800 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..101679e --- /dev/null +++ b/debian/control @@ -0,0 +1,26 @@ +Source: ceph-deploy +Maintainer: Sage Weil +Uploaders: Sage Weil +Section: admin +Priority: optional +Build-Depends: debhelper (>= 10), + dh-python, + python3, + python3-mock, + python3-remoto +X-Python-Version: >= 3.7 +Standards-Version: 4.2.1 +Homepage: http://ceph.com/ +Vcs-Git: git://github.com/ceph/ceph-deploy.git +Vcs-Browser: https://github.com/ceph/ceph-deploy + +Package: ceph-deploy +Architecture: all +Depends: ${misc:Depends}, + ${python3:Depends} +Description: Ceph-deploy is an easy to use configuration tool + for the Ceph distributed storage system. + . + This package includes the programs and libraries to support + simple ceph cluster deployment. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..93bc530 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,3 @@ +Files: * +Copyright: (c) 2004-2012 by Sage Weil +License: LGPL2.1 (see /usr/share/common-licenses/LGPL-2.1) diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..6e81be8 --- /dev/null +++ b/debian/rules @@ -0,0 +1,12 @@ +#!/usr/bin/make -f + +#export DH_VERBOSE=1 +export PYBUILD_NAME=ceph-deploy +export PYBUILD_INSTALL_ARGS_python3=--install-lib=/usr/share/ceph-deploy + +%: + dh $@ --buildsystem pybuild --with python3 + +override_dh_clean: + rm -rf ceph_deploy/lib/remoto + dh_clean diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..d3827e7 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +1.0 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..f8e0867 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ceph-deploy.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ceph-deploy.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/ceph-deploy" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ceph-deploy" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/source/_static/.empty b/docs/source/_static/.empty new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/_themes/ceph/static/font/ApexSans-Book.eot b/docs/source/_themes/ceph/static/font/ApexSans-Book.eot new file mode 100644 index 0000000000000000000000000000000000000000..332c8cbe31ad7a62f019a7c29cb82afb33c5b328 GIT binary patch literal 199888 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

@Vk=Yd;MzWy?&OsSC+4_Gy2 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/ceph/static/font/ApexSans-Book.svg b/docs/source/_themes/ceph/static/font/ApexSans-Book.svg new file mode 100644 index 0000000..8af9af2 --- /dev/null +++ b/docs/source/_themes/ceph/static/font/ApexSans-Book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/source/_themes/ceph/static/font/ApexSans-Book.ttf b/docs/source/_themes/ceph/static/font/ApexSans-Book.ttf new file mode 100644 index 0000000000000000000000000000000000000000..42a008463e6a953c99b882f8fe78edffbdfba14f GIT binary patch literal 199616 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/ceph/static/font/ApexSans-Medium.eot b/docs/source/_themes/ceph/static/font/ApexSans-Medium.eot new file mode 100644 index 0000000000000000000000000000000000000000..e06fd215408b736e671061bf0d89c3e1300f3b78 GIT binary patch literal 169448 zcmeFad0<>+xj+8A=ght(vrlG{$t=lC_N1BYN!v-&raN6|OA1|*wrMFXPdpA}V;%i-3sBRj+app=bp<`F@`FoHLoEDa-eJ@9(c);mtX7 z&bvR$`)u!f{_tsqo;je9Acgp!A}nMw3n5fNhXg-avHBjO%a$c5&%gNJexd}?b3eR& z?A*8HgK2*fG)VhsoX$t4<7tdeqqA_g5>Ix}ZaRlfMQ#tR#qVACwHG`(bqjhwFywh^*#oIBNhfwnU`ja@S zH|UT+Dx=FtrG1N6uR8wCZf7ScH`ULZ>+@FsJ zquWm%+xPgT7p@|5M2JieZ$J0!gj(kMDNzN#&+Oc{>s0r_H+Eqjo_o}ml5*0WCy$Tqc*C`+3whr~>N*(@?0>P~x9Gs{nv+jG`}|Wse(ulk`*|W;e(&kq z$CQni51{@#@O$B@W9RQve(!i2??;e7aoX6a<8z*U{;Q;1d=2{Z$$h7vb@spS{+B06 zxg7l~ckDZJe4po?C%=I5b;!R`j@?4^*12Q1Z!H^mi=65o@SOhTl)FrP{lOV~_Dw!s z_`7-7^lSXI19f<|&>U+2b>Z(s=8GpEpS;IBtUs48`Hxc0Pw@9N!ff#aYM@?#i0B;0 z8u>(3GU5=K@#61;6QPvPffMEB$=u}qf2pLOfxUE&=@d2JHCH~m^z z6F}AP4E5#Lrszp3BLyr&2T5BTl7uj4iDmLR;UY zI*f0@5_u#q(X$8_j{Qfqr7xQV4xC`a3!m}P)ua4sS2-4$tf2Mx@0*^I(QXu}LFOOkGU8Ks_dDvB<)SpG2&zz?K(bMpqEdzU8KuEa zDn%oW;aVroq%=K38J-{9XVjNzt2lssNa)i}^TpIH=1{kK2MyCBlLgKz&YUc8UU36p za)<_ii37;HUYU<_=!1F+eq*dW�H-!t-y%+%_WL4&_c-g0flhXN>m%tp;2Zcz&fg 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 literal 0 HcmV?d00001 diff --git a/docs/source/_themes/ceph/static/font/ApexSans-Medium.svg b/docs/source/_themes/ceph/static/font/ApexSans-Medium.svg new file mode 100644 index 0000000..6c624ec --- /dev/null +++ b/docs/source/_themes/ceph/static/font/ApexSans-Medium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/source/_themes/ceph/static/font/ApexSans-Medium.ttf b/docs/source/_themes/ceph/static/font/ApexSans-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..44c281e3391a61360b46b3a144eaf81dc778d0f3 GIT binary patch literal 169168 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 #`K`UsnI3zM{UazNX%y9#L;nzpdV^-lkro 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 O&o>WWy(U*!r*E{-zpeKG literal 0 HcmV?d00001 diff --git a/docs/source/_themes/ceph/static/nature.css_t b/docs/source/_themes/ceph/static/nature.css_t new file mode 100644 index 0000000..394a633 --- /dev/null +++ b/docs/source/_themes/ceph/static/nature.css_t @@ -0,0 +1,325 @@ +/* + * nature.css_t + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- nature theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +@font-face { + font-family: 'ApexSansMedium'; + src: url('font/ApexSans-Medium.eot'); + src: url('font/ApexSans-Medium.eot?#iefix') format('embedded-opentype'), + url('font/ApexSans-Medium.woff') format('woff'), + url('font/ApexSans-Medium.ttf') format('truetype'), + url('font/ApexSans-Medium.svg#FontAwesome') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'ApexSansBook'; + src: url('font/ApexSans-Book.eot'); + src: url('font/ApexSans-Book.eot?#iefix') format('embedded-opentype'), + url('font/ApexSans-Book.woff') format('woff'), + url('font/ApexSans-Book.ttf') format('truetype'), + url('font/ApexSans-Book.svg#FontAwesome') format('svg'); + font-weight: normal; + font-style: normal; +} + +body { + font: 14px/1.4 Helvetica, Arial, sans-serif; + background-color: #E6E8E8; + color: #37424A; + margin: 0; + padding: 0; + border-top: 5px solid #F05C56; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 330px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.document { + background-color: #ffffff; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; +} + +div.footer { + color: #222B31; + width: 100%; + padding: 13px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #444; + text-decoration: underline; +} + +div.related { + background-color: #80D2DC; + line-height: 32px; + color: #37424A; + // text-shadow: 0px 1px 0 #444; + font-size: 100%; + border-top: #9C4850 5px solid; +} + +div.related a { + color: #37424A; + text-decoration: none; +} + +div.related a:hover { + color: #fff; + // text-decoration: underline; +} + +div.sphinxsidebar { + // font-size: 100%; + line-height: 1.5em; + width: 330px; +} + +div.sphinxsidebarwrapper{ + padding: 20px 0; + background-color: #efefef; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: ApexSansMedium; + color: #e6e8e8; + font-size: 1.2em; + font-weight: normal; + margin: 0; + padding: 5px 10px; + background-color: #5e6a71; + // text-shadow: 1px 1px 0 white; + text-transform: uppercase; +} + +div.sphinxsidebar h4{ + font-size: 1.1em; +} + +div.sphinxsidebar h3 a { + color: #e6e8e8; +} + + +div.sphinxsidebar p { + color: #888; + padding: 5px 20px; +} + +div.sphinxsidebar p.topless { +} + +div.sphinxsidebar ul { + margin: 10px 5px 10px 20px; + padding: 0; + color: #000; +} + +div.sphinxsidebar a { + color: #444; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar input[type=text]{ + margin-left: 20px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #F05C56; + text-decoration: none; +} + +a:hover { + color: #F05C56; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + // font-family: ApexSansMedium; + // background-color: #80D2DC; + // font-weight: normal; + // color: #37424a; + margin: 30px 0px 10px 0px; + padding: 5px 0 5px 0px; + // text-shadow: 0px 1px 0 white; + text-transform: uppercase; +} + +div.body h1 { font: 20px/2.0 ApexSansBook; color: #37424A; border-top: 20px solid white; margin-top: 0; } +div.body h2 { font: 18px/1.8 ApexSansMedium; background-color: #5E6A71; color: #E6E8E8; padding: 5px 10px; } +div.body h3 { font: 16px/1.6 ApexSansMedium; color: #37424A; } +div.body h4 { font: 14px/1.4 Helvetica, Arial, sans-serif; color: #37424A; } +div.body h5 { font: 12px/1.2 Helvetica, Arial, sans-serif; color: #37424A; } +div.body h6 { font-size: 100%; color: #37424A; } + +// div.body h2 { font-size: 150%; background-color: #E6E8E8; color: #37424A; } +// div.body h3 { font-size: 120%; background-color: #E6E8E8; color: #37424A; } +// div.body h4 { font-size: 110%; background-color: #E6E8E8; color: #37424A; } +// div.body h5 { font-size: 100%; background-color: #E6E8E8; color: #37424A; } +// div.body h6 { font-size: 100%; background-color: #E6E8E8; color: #37424A; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + line-height: 1.5em; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.highlight{ + background-color: white; +} + +div.note { + background-color: #e6e8e8; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #efefef; +} + +div.warning { + background-color: #F05C56; + border: 1px solid #9C4850; + color: #fff; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 10px; + background-color: White; + color: #222; + line-height: 1.2em; + border: 1px solid #5e6a71; + font-size: 1.1em; + margin: 1.5em; + -webkit-box-shadow: 1px 1px 1px #e6e8e8; + -moz-box-shadow: 1px 1px 1px #e6e8e8; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ + font-size: 15px; + font-family: monospace; +} + +.viewcode-back { + font-family: Arial, sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} + +table.docutils { + margin: 1.5em; +} + +div.sidebar { + border: 1px solid #5E6A71; + background-color: #E6E8E8; +} + +div.admonition.tip { + background-color: #80D2DC; + border: 1px solid #55AEBA; +} + +div.admonition.important { + background-color: #F05C56; + border: 1px solid #9C4850; + color: #fff; +} + +div.tip tt.literal { + background-color: #55aeba; + color: #fff; +} + +div.important tt.literal { + background-color: #9C4850; + color: #fff; +} + +h2 .literal { + color: #fff; + background-color: #37424a; +} + +dl.glossary dt { + font-size: 1.0em; + padding-top:20px; + +} \ No newline at end of file diff --git a/docs/source/_themes/ceph/theme.conf b/docs/source/_themes/ceph/theme.conf new file mode 100644 index 0000000..1cc4004 --- /dev/null +++ b/docs/source/_themes/ceph/theme.conf @@ -0,0 +1,4 @@ +[theme] +inherit = basic +stylesheet = nature.css +pygments_style = tango diff --git a/docs/source/admin.rst b/docs/source/admin.rst new file mode 100644 index 0000000..8ae7395 --- /dev/null +++ b/docs/source/admin.rst @@ -0,0 +1,26 @@ +.. _admin: + +admin +======= +The ``admin`` subcommand provides an interface to add to the cluster's admin +node. + +Example +------- +To make a node and admin node run:: + + ceph-deploy admin ADMIN [ADMIN..] + +This places the the cluster configuration and the admin keyring on the remote +nodes. + +Admin node definition +--------------------- + +The definition of an admin node is that both the cluster configuration file +and the admin keyring. Both of these files are stored in the directory +/etc/ceph and thier prefix is that of the cluster name. + +The default ceph cluster name is "ceph". So with a cluster with a default name +the admin keyring is named /etc/ceph/ceph.client.admin.keyring while cluster +configuration file is named /etc/ceph/ceph.conf. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 0000000..7c0ec67 --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,623 @@ +Changelog +========= + +2.1 +--- + +2.1.0 +^^^^^ +02-Oct-2020 + +* add Python3 support +* build python2-ceph-deploy and python3-ceph-deply rpm packages + +2.0 +--- + +2.0.2 +^^^^^ +16-Jul-2018 + +* Bump the ``remoto`` requirement that fixes the ``expand_env`` bug + + +2.0.1 +^^^^^ +19-Jun-2018 + +* Add support for archlinux +* Support IPV6 addresses in monitors +* Add debug argument when calling disk zap +* Ensure remote executables are files (vs. possible dirs) +* Run ``apt-get update`` before installs +* Default to mimic release +* Use INFO log levels for disk list +* Fix ``UnboundLocalError`` when createing mds/mgr with bad hosts +* Improve distro detection for Arch Linux +* Add epilog text + + +2.0.0 +^^^^^ +16-Jan-2018 + +* Backward incompatible API changes for OSD creation - will use ceph-volume and + no longer consume ceph-disk. +* Remove python-distribute dependency +* Use /etc/os-release as a fallback when ``linux_distribution()`` doesn't work +* Drop dmcrypt support (unsupported by ceph-volume for now) +* Allow debug modes for ceph-volume + + +1.5 +--- + +1.5.39 +^^^^^^ +1-Sep-2017 + +* Remove ``--cluster`` options, default to ``ceph`` always +* Add ``--filestore`` since ``ceph-disk`` defaults to bluestore +* Start testing against Python 3.5 +* Support Debian 9 and 10 intalls +* Better handling on package conflicts when upgrading/re-installing + + +1.5.38 +^^^^^^ +19-May-2017 + +* Allow unsigned deb packages from mirrors +* Detect systemd before sysvinit in centos +* Fix UnboundLocalError when installing in debian with custom repo flags +* gatherkeys to give mgr "allow * " permissions +* specify block.db and block.wal for bluestore +* be able to install ceph-mgr +* bootstrap mgr keys +* cleanup mds key creation +* Virtuozzo Linux support +* update osd and mds caps + + +1.5.37 +^^^^^^ +03-Jan-2017 + +* Use the ``--cluster`` flag on monitor commands (defaulting to 'ceph' if + unspecfied) +* After adding a monitor, ensure it is started regardless of init system +* Allow Oracle Linux Server to be deployed to. +* Fix issue when calling gatherkeys where a log argument was missing +* Use the new development services for installation (from chacra.ceph.com and + shaman.ceph.com URLs) +* Try to decode bytes only on Python 3 when writing files on remote hosts + + +1.5.36 +^^^^^^ +29-Aug-2016 + +* Prefer to use ``load_raw`` to avoid mangling ceph.conf content. +* Improve systemd/sysvinit detection for both CentOS and RHEL +* Gatherkeys should try to get an existing key without caps, in case they don't + match + +1.5.35 +^^^^^^ +15-Aug-2016 + +* Add compatibility for bytes/strings with Python 3 +* Fix errors in argparse default behavior (error messages, incomplete commands) +* Add Python 3.4 to tox +* Python 3 changes to workaround configparser issues +* Use the configured username when using rsync to a remote host (local repo + support) +* Install Python 3 with the bootstrap sciprt +* Bump remoto requirement to 0.0.29 +* Include admin.rst and gatherkeys.rst in the TOC index +* Handle Ceph package split in Ubuntu +* Add a ``--nogpgcheck`` option to disable checks on local repos +* Improve sysvinit/systemd checks by not including 'ceph' in the path +* Install Diamond when calling ``ceph-deploy calamari connect`` +* Zypper fixes for purging: allows removal of multiple packages + + +07-Jun-2016 +1.5.34 +^^^^^^ +07-Jun-2016 + +* Do not call partx/partprobe when zapping disks +* No longer allow using ext4 +* Default to systemd for SUSE +* Remove usage of rcceph (for SUSE) +* No longer depend on automatic ``ceph-create-keys``, use the monitors to fetch + keys. +* Use ``0.0.28`` from remoto + +1.5.33 +^^^^^^ +22-Apr-2016 + +* Default to Jewel for releases + +1.5.32 +^^^^^^ +13-Apr-2016 + +* Improve systemd detection for Ubuntu releases. +* Rename ceph-deploy log to include the cluster name +* Bluestore support +* Disable timeouts for pkg install/remove operations (they can take a long + time) +* Remove deprecated ceph.conf configuration "filestore xattr use omap = true" + +1.5.31 +^^^^^^ +04-Jan-2016 + +* Use the new remoto version (0.0.27) that fixes an error when dealing with + remote output. + +1.5.30 +^^^^^^ +11-Dec-2015 + +* Default to the "infernalis" release. +* Fix an issue when trying to destroy/stop monitors on systemd servers + +1.5.29 +^^^^^^ +2-Dec-2015 + +* Add support for ``--dev-commit `` +* Add ``--test`` option for installing ceph-test package +* Enable Ceph on ``osd create`` +* Remove bootstrap-rgw key when forgetkeys is used +* Prefer systemd over upstart in newer Ubuntu +* Use download.ceph.com directly +* Use better examples in default cephdeploy.conf file +* Cleanup functions for uninstall and purge (simplifying code) +* Use https for download.cep.com +* Fix gitbuilder hosts to avoid using https +* Do not udevadm trigger because ceph-disk does it already +* Download gpg keys from download.ceph.com +* Specify a PID location for monitors +* Fix invalid path for release keys in test +* Add timestamp to log output + +1.5.28 +^^^^^^ +26-Aug-2015 + +* Fix issue when importing GPG keys on Centos 6 introduced in 1.5.27. +* Support systemd and sysvinit on RHEL, Fedora, and CentOS, when systemd + is present in the Ceph packages. +* Simplify steps taken when adding a monitor with ``ceph-deploy mon add``. + Eliminates a 5-minute hang when moving from 1 monitor to 2. +* Make sure that Ceph is installed on a remote node before trying to enable + a Ceph daemon. + +1.5.27 +^^^^^^ +05-Aug-2015 + +* New ``repo`` top-level command for adding and removing repos. +* Ability to install subset of ceph packages based on CLI switches like + ``--cli``, ``--rgw``, etc. +* Initial support for systemd. Ceph on Fedora 22 only. +* Fixed an issue that prevented package upgrades when using DNF. +* No longer installs yum-priorities-plugin when using DNF. + +1.5.26 +^^^^^^ +20-Jul-2015 + +* Make parsing of boolean values in config file overrides work. +* Output value of all ceph-deploy options upon invocation. +* Point to git.ceph.com for GPG keys. +* Make GPG key fetching work on Debian Wheezy. +* Allow ceph-deploy to work on Mint distro. +* Improved help menu output during subcommand context. +* Point to SUSE downstream packages by default on SUSE distros since + ceph.com does not host packages for SUSE anymore.. +* Some initial groundwork for installing Ceph daemons that will no longer + run as root user. +* Add support for DNF package manager (Fedora >= 22 only). +* Echo RGW default port number after ``ceph-deploy rgw create``. + +1.5.25 +^^^^^^ +26-May-2015 + +* **CVE-2015-4053**: Make sure that the admin keyring is mode 0600 after being + pushed with the ``ceph-deploy admin`` command. +* Improved SUSE install and purge. +* Make sure that package name 'ceph-radosgw' is used everywhere for RPM systems + instead of 'radosgw'. + +1.5.24 +^^^^^^ +18-May-2015 + +* Use version 0.0.25 of ``remoto`` that fixes an issue where output would be cut + (https://github.com/alfredodeza/remoto/issues/15). +* Automatically prefix custom RGW daemon names with 'rgw.' +* Log an error message when deploying MDS in RHEL distros fails as it may not + be supported. +* More robust vendor.py script (tries ceph.com and GitHub) +* Create /var/lib/ceph/radosgw directory on remote host if not present +* Enable/start ceph-radosgw service on RPM systems instead of radosgw +* Add flags to support install of specific daemons (OSD, MON, RGW, MDS) only + Note that the packaging changes for this in upstream Ceph are still pending +* removing installation of 'calamari-minions' repo upon + 'ceph-deploy calamari connect' +* enable ceph-mds service correctly on systemd +* Check for sysvinit and custom cluster name on 'ceph-deploy new' command + +1.5.23 +^^^^^^ +07-Apr-2015 + +* Default to Hammer on install. +* Add ``rgw`` command to easily create rgw instances. +* Automatically install the radosgw package. +* Remove unimplemented subcommands from CLI and help. +* **CVE-2015-3010**: Fix an issue where keyring permissions were + world readable (thanks Owen Synge). +* Fix an issue preventing all but the first host given to + ``install --repo`` from being used. + +1.5.22 +^^^^^^ +09-Mar-2015 + +* Enable ``check_obsoletes`` in Yum priorities plugin when deploying + upstream Ceph on RPM-based distros. +* Require ``--release`` flag to install upstream Ceph on RHEL. +* Uninstall ``ceph-common`` on Fedora. + +1.5.21 +^^^^^^ +10-Dec-2014 + +* Fix distro detection for CentOS and Scientific Linux, which was + preventing installation of EPEL repo as a prerequisite. +* Default to Giant on install. +* Fix an issue where ``gatherkeys`` did not exit non-zero when + keys were not found. + +1.5.20 +^^^^^^ +13-Nov-2014 + +* log stderr and stdout in the same order as they happen remotely. + +1.5.19 +^^^^^^ +29-Oct-2014 + +* Create temporary ceph.conf files in ``/etc/ceph`` to avoid issues with + SELinux. + +1.5.18 +^^^^^^ +09-Oct-2014 + +* Fix issue for enabling the OSD service in el-like distros. +* Create a monitor keyring if it doesn't exist. + +1.5.17 +^^^^^^ +06-Oct-2014 + +* Do not ask twice for passwords when calling ``new``. +* Ensure priorities are installed and enforced for custom repositories. + +1.5.16 +^^^^^^ +30-Sep-2014 + +* Enable services on ``el`` distros when deploying Ceph daemons. +* Smarter detection of ``sudo`` need on remote nodes (prevents issues when + running ceph-deploy as ``root`` or with ``sudo``. +* Fix an issue where Debian Sid would break ceph-deploy failing Distro + detection. + +1.5.15 +^^^^^^ +12-Sep-2014 + +* If ``wget`` is installed don't try to install it regardless. + +1.5.14 +^^^^^^ +09-Sep-2014 + +* Do not override environment variables on remote hosts, preserve them and + extend the ``$PATH`` if not explicitly told not to. + +1.5.13 +^^^^^^ +03-Sep-2014 + +* Fix missing priority plugin in YUM for Fedora when installing +* Implement --public-network and --cluster-network with remote IP validation +* Fixed an issue where errors before the logger was setup would be silenced. + +1.5.12 +^^^^^^ +25-Aug-2014 + +* Better traceback reporting with logging. +* Close stderr/stdout when ceph-deploy completes operations (silences odd + tracebacks) +* Allow to re-use a ceph.conf file with ``--ceph-conf`` global flag +* Be able to concatenate and seed keyring files with ``--keyrings`` + +1.5.11 +^^^^^^ +25-Aug-2014 + +* Fix a problem where CentOS7 is not matched correctly against repos (Thanks + Tom Walsh) + +1.5.10 +^^^^^^ +31-Jul-2014 + +* Use ``ceph-disk`` with high verbosity +* Don't require ``ceph-common`` on EL distros +* Use ``ceph-disk zap`` instead of re-implementing it +* Use proper paths for ``zypper`` (Thanks Owen Synge) +* More robust ``init`` detection for Ubuntu (Thanks Joao Eduardo Luis) +* Allow to install repo files only +* Work with inconsistent repo sections for Emperor when setting priorities + +1.5.9 +^^^^^ +14-Jul-2014 + +* Allow to optionally set the ``fsid`` when calling ``new`` +* Correctly select sysvinit or systemd for Suse versions (Thanks Owen Synge) +* Use correct version of remoto (``0.0.19``) that holds the ``None`` global fix +* Fix new naming scheme for CentOS platforms that prevented CentOS 7 installs + +1.5.8 +^^^^^ +09-Jul-2014 + +* Create a flake8/pep8/linting job so that we prevent Undefined errors +* Add partprobe/partx calls when zapping disks +* Fix RHEL7 installation issues (url was using el6 incorrectly) (Thanks David Vossel) +* Warn when an executable is not found +* Fix an ``AttributeError`` in execnet (see https://github.com/alfredodeza/execnet/issues/1) + +1.5.7 +^^^^^ +01-Jul-2014 + +* Fix ``NameError`` on osd.py from an undefined variable +* Fix a calamari connect problem when installing on multiple hosts + +1.5.6 +^^^^^ +01-Jul-2014 + +* Optionally avoid vendoring libraries for upstream package maintainers. +* Fix RHEL7 installation issue that was pulling ``el6`` packages (Thanks David Vossel) + +1.5.5 +^^^^^ +10-Jun-2014 + +* Normalize repo file header calls. Fixes breakage on Calamari repos. + +1.5.4 +^^^^^ +10-Jun-2014 + +* Improve help by adding online doc link +* allow cephdeploy.conf to set priorities in repos +* install priorities plugin for yum distros +* set the right priority for ceph.repo and warn about this + +1.5.3 +^^^^^ +30-May-2014 + +* Another fix for IPV6: write correct ``mon_host`` in ceph.conf +* Support ``proxy`` settings for repo files in YUM +* Better error message when ceph.conf is not found +* Refuse to install custom cluster names on sysvinit systems (not supported) +* Remove quiet flags from package manager's install calls to avoid timing out +* Use the correct URL repo when installing for RHEL + +1.5.2 +^^^^^ +09-May-2014 + +* Remove ``--`` from the command to install packages. (Thanks Vincenzo Pii) +* Default to Firefly as the latest, stable Ceph version + +1.5.1 +^^^^^ +01-May-2014 + +* Fixes a broken ``osd`` command that had the wrong attribute in the conn + object + +1.5.0 +^^^^^ +28-Apr-2014 + +* Warn if ``requiretty`` is causing issues +* Support IPV6 host resolution (Thanks Frode Nordahl) +* Fix incorrect paths for local cephdeploy.conf +* Support subcommand overrides defined in cephdeploy.conf +* When installing on CentOS/RHEL call ``yum clean all`` +* Check OSD status when deploying to catch possible issues +* Add a ``--local-mirror`` flag for installation that syncs files +* Implement ``osd list`` to list remote osds +* Fix install issues on Suse (Thanks Owen Synge) + +1.4 +----- + +1.4.0 +^^^^^ +* uninstall ceph-release and clean cache in CentOS +* Add ability to add monitors to an existing cluster +* Deprecate use of ``--stable`` for releases, introduce ``--release`` +* Eat some tracebacks that may appear when closing remote connections +* Enable default ceph-deploy configurations for repo handling +* Fix wrong URL for rpm installs with ``--testing`` flag + +1.3 +--- + +1.3.5 +^^^^^ +* Support Debian SID for installs +* Error nicely when hosts cannot be resolved +* Return a non-zero exit status when monitors have not formed quorum +* Use the new upstream library for remote connections (execnet 1.2) +* Ensure proper read permissions for ceph.conf when pushing configs +* clean up color logging for non-tty sessions +* do not reformat configs when pushing, pushes are now as-is +* remove dry-run flag that did nothing + +1.3.4 +^^^^^ +* ``/etc/ceph`` now gets completely removed when using ``purgedata``. +* Refuse to perform ``purgedata`` if ceph is installed +* Add more details when a given platform is not supported +* Use new Ceph auth settings for ``ceph.conf`` +* Remove old journal size settings from ``ceph.conf`` +* Add a new subcommand: ``pkg`` to install/remove packages from hosts + + +1.3.3 +^^^^^ +* Add repo mirror support with ``--repo-url`` and ``--gpg-url`` +* Remove dependency on the ``which`` command +* Fix problem when removing ``/var/lib/ceph`` and OSDs are still mounted +* Make sure all tmp files are closed before moving, fixes issue when creating + keyrings and conf files +* Complete remove the lsb module + + +1.3.2 +^^^^^ +* ``ceph-deploy new`` will now attempt to copy SSH keys if necessary unless it + it disabled. +* Default to Emperor version of ceph when installing. + +1.3.1 +^^^^^ +* Use ``shutil.move`` to overwrite files from temporary ones (Thanks Mark + Kirkwood) +* Fix failure to ``wget`` GPG keys on Debian and Debian-based distros when + installing + +1.3.0 +^^^^^ +* Major refactoring for all the remote connections in ceph-deploy. With global + and granular timeouts. +* Raise the log level for missing keyrings +* Allow ``--username`` to be used for connecting over SSH +* Increase verbosity when MDS fails, include the exit code +* Do not remove ``/etc/ceph``, just the contents +* Use ``rcceph`` instead of service for SUSE +* Fix lack of ``--cluster`` usage on monitor error checks +* ensure we correctly detect Debian releases + +1.2 +--- + +1.2.7 +^^^^^ +* Ensure local calls to ceph-deploy do not attempt to ssh. +* ``mon create-initial`` command to deploy all defined mons, wait for them to + form quorum and finally to gatherkeys. +* Improve help menu for mon commands. +* Add ``--fs-type`` option to ``disk`` and ``osd`` commands (Thanks Benoit + Knecht) +* Make sure we are using ``--cluster`` for remote configs when starting ceph +* Fix broken ``mon destroy`` calls using the new hostname resolution helper +* Add a helper to catch common monitor errors (reporting the status of a mon) +* Normalize all configuration options in ceph-deploy (Thanks Andrew Woodward) +* Use a ``cuttlefish`` compatible ``mon_status`` command +* Make ``osd activate`` use the new remote connection libraries for improved + readability. +* Make ``disk zap`` also use the new remote connection libraries. +* Handle any connection errors that may came up when attempting to get into + remote hosts. + +1.2.6 +^^^^^ +* Fixes a problem witha closed connection for Debian distros when creating + a mon. + +1.2.5 +^^^^^ +* Fix yet another hanging problem when starting monitors. Closing the + connection now before we even start them. + +1.2.4 +^^^^^ +* Improve ``osd help`` menu with path information +* Really discourage the use of ``ceph-deploy new [IP]`` +* Fix hanging remote requests +* Add ``mon status`` output when creating monitors +* Fix Debian install issue (wrong parameter order) (Thanks Sayid Munawar) +* ``osd`` commands will be more verbose when deploying them +* Issue a warning when provided hosts do not match ``hostname -s`` remotely +* Create two flags for altering/not-altering source repos at install time: + ``--adjust-repos`` and ``--no-adjust-repos`` +* Do not do any ``sudo`` commands if user is root +* Use ``mon status`` for every ``mon`` deployment and detect problems with + monitors. +* Allow to specify ``host:fqdn/ip`` for all mon commands (Thanks Dmitry + Borodaenko) +* Be consistent for hostname detection (Thanks Dmitry Borodaenko) +* Fix hanging problem on remote hosts + +1.2.3 +^^^^^ +* Fix non-working ``disk list`` +* ``check_call`` utility fixes ``$PATH`` issues. +* Use proper exit codes from the ``main()`` CLI function +* Do not error when attempting to add the EPEL repos. +* Do not complain when using IP:HOST pairs +* Report nicely when ``HOST:DISK`` is not used when zapping. + +1.2.2 +^^^^^ +* Do not force usage of lsb_release, fallback to + ``platform.linux_distribution()`` +* Ease installation in CentOS/Scientific by adding the EPEL repo + before attempting to install Ceph. +* Graceful handling of pushy connection issues due to host + address resolution +* Honor the usage of ``--cluster`` when calling osd prepare. + +1.2.1 +^^^^^ +* Print the help when no arguments are passed +* Add a ``--version`` flag +* Show the version in the help menu +* Catch ``DeployError`` exceptions nicely with the logger +* Fix blocked command when calling ``mon create`` +* default to ``dumpling`` for installs +* halt execution on remote exceptions + +1.2.0 +^^^^^ +* Better logging output +* Remote logging for individual actions for ``install`` and ``mon create`` +* Install ``ca-certificates`` on all Debian-based distros +* Honor the usage of ``--cluster`` +* Do not ``rm -rf`` monitor logs when destroying +* Error out when ``ceph-deploy new [IP]`` is used +* Log the ceph version when installing diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..6f51c15 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +# +# ceph-deploy documentation build configuration file, created by +# sphinx-quickstart on Mon Oct 21 09:32:42 2013. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.append(os.path.abspath('_themes')) +sys.path.insert(0, os.path.abspath('../..')) +import ceph_deploy + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'contents' + +# General information about the project. +project = u'ceph-deploy' +copyright = u'2013, Inktank' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = ceph_deploy.__version__ +# The full version, including alpha/beta/rc tags. +release = ceph_deploy.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'ceph' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ['_themes'] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +html_use_smartypants = False + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'ceph-deploydoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'ceph-deploy.tex', u'ceph-deploy Documentation', + u'Inktank', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'ceph-deploy', u'ceph-deploy Documentation', + [u'Inktank'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'ceph-deploy', u'ceph-deploy Documentation', + u'Inktank', 'ceph-deploy', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# XXX Uncomment when we are ready to link to ceph docs +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/docs/source/conf.rst b/docs/source/conf.rst new file mode 100644 index 0000000..6eee392 --- /dev/null +++ b/docs/source/conf.rst @@ -0,0 +1,175 @@ +.. _conf: + +Ceph Deploy Configuration +========================= +Starting with version 1.4, ceph-deploy uses a configuration file that can be +one of: + +* ``cephdeploy.conf`` (in the current directory) +* ``$HOME/.cephdeploy.conf`` (hidden in the user's home directory) + +This configuration file allows for setting certain ceph-deploy behavior that +would be difficult to set on the command line or that it might be cumbersome to +do. + +The file itself follows the INI style of configurations which means that it +consists of sections (in brackets) that may contain any number of key/value +pairs. + +If a configuration file is not found in the current working directory nor in +the user's home dir, ceph-deploy will proceed to create one in the home +directory. + +This is how a default configuration file would look like:: + + # + # ceph-deploy configuration file + # + + [ceph-deploy-global] + # Overrides for some of ceph-deploy's global flags, like verbosity or cluster + # name + + [ceph-deploy-install] + # Overrides for some of ceph-deploy's install flags, like version of ceph to + # install + + + # + # Repositories section + # + + # yum repos: + # [myrepo] + # baseurl = https://user:pass@example.org/rhel6 + # gpgurl = https://example.org/keys/release.asc + # default = True + # extra-repos = cephrepo # will install the cephrepo file too + # + # [cephrepo] + # name=ceph repo noarch packages + # baseurl=http://ceph.com/rpm-emperor/el6/noarch + # enabled=1 + # gpgcheck=1 + # type=rpm-md + # gpgkey=https://download.ceph.com/keys/release.asc + + # apt repos: + # [myrepo] + # baseurl = https://user:pass@example.org/ + # gpgurl = https://example.org/keys/release.asc + # default = True + # extra-repos = cephrepo # will install the cephrepo file too + # + # [cephrepo] + # baseurl=http://ceph.com/rpm-emperor/el6/noarch + # gpgkey=https://download.ceph.com/keys/release.asc + +.. conf_sections: + +Sections +-------- +To work with ceph-deploy configurations, it is important to note that all +sections that relate to ceph-deploy's flags and state are prefixed with +``ceph-deploy-`` followed by the subcommand or by ``global`` if it is something +that belongs to the global flags. + +Any other section that is not prefixed with ``ceph-deploy-`` is considered +a repository. + +Repositories can be very complex to describe and most of the time (specially +for yum repositories) they can be very verbose too. + +Setting Default Flags or Values +------------------------------- +Because the configuration loading allows specifying the same flags as in the +CLI it is possible to set defaults. For example, assuming that a user always +wants to install Ceph the following way (that doesn't create/modify remote repo +files):: + + ceph-deploy install --no-adjust-repos {nodes} + +This can be the default behavior by setting it in the right section in the +configuration file, which should look like this:: + + [ceph-deploy-install] + adjust_repos = False + +The default for ``adjust_repos`` is ``True``, but because we are changing this +to ``False`` the CLI will now have this behavior changed without the need to +pass any flag. + +Repository Sections +------------------- +Keys will depend on the type of package manager that will use it. Certain keys +for yum are required (like ``baseurl``) and some others like ``gpgcheck`` are +optional. + +For both yum and apt these would be all the required keys in a repository section: + +* baseurl +* gpgkey + +If a required key is not present ceph-deploy will abort the installation +process with an error identifying the section and key what was missing. + +In yum the repository name is taken from the section, so if the section is +``[foo]``, then the name of the repository will be ``foo repo`` and the +filename written to ``/etc/yum.repos.d/`` will be ``foo.repo``. + +For apt, the same happens except the directory location changes to: +``/etc/apt/sources.list.d/`` and the file becomes ``foo.list``. + + +Optional values for yum +----------------------- +**name**: A descriptive name for the repository. If not provided ``{repo +section} repo`` is used + +**enabled**: Defaults to ``1`` + +**gpgcheck**: Defaults to ``1`` + +**type**: Defaults to ``rpm-md`` + +**gpgcheck**: Defaults to ``1`` + + +Default Repository +------------------ +For installations where a default repository is needed a key can be added to +that section to indicate it is the default one:: + + [myrepo] + default = true + +When a default repository is detected it is mentioned in the log output and +ceph will get install from that one repository at the end. + +Extra Repositories +------------------ +If other repositories need to be installed aside from the main one, a key +should be added to represent that need with a comma separated value with the +name of the sections of the other repositories (just like the example +configuration file demonstrates):: + + [myrepo] + baseurl = https://user:pass@example.org/rhel6 + gpgurl = https://example.org/keys/release.asc + default = True + extra-repos = cephrepo # will install the cephrepo file too + + [cephrepo] + name=ceph repo noarch packages + baseurl=http://ceph.com/rpm-emperor/el6/noarch + enabled=1 + gpgcheck=1 + type=rpm-md + gpgkey=https://download.ceph.com/keys/release.asc + +In this case, the repository called ``myrepo`` defines the ``extra-repos`` key +with just one extra one: ``cephrepo``. + +This extra repository must exist as a section in the configuration file. After +the main one is added all the extra ones defined will follow. Installation of +Ceph will only happen with the main repository. diff --git a/docs/source/contents.rst b/docs/source/contents.rst new file mode 100644 index 0000000..c80d8cc --- /dev/null +++ b/docs/source/contents.rst @@ -0,0 +1,18 @@ +Content Index +============= + +.. toctree:: + :maxdepth: 2 + + index.rst + new.rst + install.rst + mon.rst + rgw.rst + mds.rst + conf.rst + pkg.rst + repo.rst + changelog.rst + admin.rst + gatherkeys.rst diff --git a/docs/source/gatherkeys.rst b/docs/source/gatherkeys.rst new file mode 100644 index 0000000..6a1bdea --- /dev/null +++ b/docs/source/gatherkeys.rst @@ -0,0 +1,55 @@ +.. _gatherkeys: + +========== +gatherkeys +========== + +The ``gatherkeys`` subcommand provides an interface to get with a cluster's +cephx bootstrap keys. + +keyrings +======== +The ``gatherkeys`` subcommand retrieves the following keyrings. + +ceph.mon.keyring +---------------- +This keyring is used by all mon nodes to communicate with other mon nodes. + +ceph.client.admin.keyring +------------------------- +This keyring is ceph client commands by default to administer the ceph cluster. + +ceph.bootstrap-osd.keyring +-------------------------- +This keyring is used to generate cephx keyrings for OSD instances. + +ceph.bootstrap-mds.keyring +-------------------------- +This keyring is used to generate cephx keyrings for MDS instances. + +ceph.bootstrap-rgw.keyring +-------------------------- +This keyring is used to generate cephx keyrings for RGW instances. + +Example +======= +The ``gatherkeys`` subcommand contacts the mon and creates or retrieves existing +keyrings from the mon internal store. To run:: + + ceph-deploy gatherkeys MON [MON..] + +You can optionally add as many mon nodes to the command line as desired. The +``gatherkeys`` subcommand will succeed on the first mon to respond successfully +with all the keyrings. + +Backing up of old keyrings +========================== + +If old keyrings exist in the current working directory that do not match the +retrieved keyrings these old keyrings will be renamed with a time stamp +extention so you will not loose valuable keyrings. + +.. note:: Before version v1.5.33 ceph-deploy relied upon ``ceph-create-keys`` + and did not backup existing keys. Using ``ceph-create-keys`` produced + a side effect of deploying all bootstrap keys on the mon node so + making all mon nodes admin nodes. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..3ba0f99 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,315 @@ +======================================================== + ceph-deploy -- Deploy Ceph with minimal infrastructure +======================================================== + +``ceph-deploy`` is a way to deploy Ceph relying on just SSH access to +the servers, ``sudo``, and some Python. It runs fully on your +workstation, requiring no servers, databases, or anything like that. + +If you set up and tear down Ceph clusters a lot, and want minimal +extra bureaucracy, this is for you. + +.. _what this tool is not: + +What this tool is not +--------------------- +It is not a generic deployment system, it is only for Ceph, and is designed +for users who want to quickly get Ceph running with sensible initial settings +without the overhead of installing Chef, Puppet or Juju. + +It does not handle client configuration beyond pushing the Ceph config file +and users who want fine-control over security settings, partitions or directory +locations should use a tool such as Chef or Puppet. + + +Installation +============ +Depending on what type of usage you are going to have with ``ceph-deploy`` you +might want to look into the different ways to install it. For automation, you +might want to ``bootstrap`` directly. Regular users of ``ceph-deploy`` would +probably install from the OS packages or from the Python Package Index. + +Python Package Index +-------------------- +If you are familiar with Python install tools (like ``pip`` and +``easy_install``) you can easily install ``ceph-deploy`` like:: + + pip install ceph-deploy + + +It should grab all the dependencies for you and install into the current user's +environment. + +We highly recommend using ``virtualenv`` and installing dependencies in +a contained way. + + +DEB +--- +All new releases of ``ceph-deploy`` are pushed to all ``ceph`` DEB release +repos. + +The DEB release repos are found at:: + + http://ceph.com/debian-{release} + http://ceph.com/debian-testing + +This means, for example, that installing ``ceph-deploy`` from +http://ceph.com/debian-giant will install the same version as from +http://ceph.com/debian-firefly or http://ceph.com/debian-testing. + +RPM +--- +All new releases of ``ceph-deploy`` are pushed to all ``ceph`` RPM release +repos. + +The RPM release repos are found at:: + + http://ceph.com/rpm-{release} + http://ceph.com/rpm-testing + +Make sure you add the proper one for your distribution (i.e. el7 vs rhel7). + +This means, for example, that installing ``ceph-deploy`` from +http://ceph.com/rpm-giant will install the same version as from +http://ceph.com/rpm-firefly or http://ceph.com/rpm-testing. + +bootstrapping +------------- +To get the source tree ready for use, run this once:: + + ./bootstrap + +You can symlink the ``ceph-deploy`` script in this somewhere +convenient (like ``~/bin``), or add the current directory to ``PATH``, +or just always type the full path to ``ceph-deploy``. + + +SSH and Remote Connections +========================== +``ceph-deploy`` will attempt to connect via SSH to hosts when the hostnames do +not match the current host's hostname. For example, if you are connecting to +host ``node1`` it will attempt an SSH connection as long as the current host's +hostname is *not* ``node1``. + +ceph-deploy at a minimum requires that the machine from which the script is +being run can ssh as root without password into each Ceph node. + +To enable this generate a new ssh keypair for the root user with no passphrase +and place the public key (``id_rsa.pub`` or ``id_dsa.pub``) in:: + + /root/.ssh/authorized_keys + +and ensure that the following lines are in the sshd config:: + + PermitRootLogin yes + PermitEmptyPasswords yes + +The machine running ceph-deploy does not need to have the Ceph packages +installed unless it needs to admin the cluster directly using the ``ceph`` +command line tool. + + +usernames +--------- +When not specified the connection will be done with the same username as the +one executing ``ceph-deploy``. This is useful if the same username is shared in +all the nodes but can be cumbersome if that is not the case. + +A way to avoid this is to define the correct usernames to connect with in the +SSH config, but you can also use the ``--username`` flag as well:: + + ceph-deploy --username ceph install node1 + +``ceph-deploy`` then in turn would use ``ceph@node1`` to connect to that host. + +This would be the same expectation for any action that warrants a connection to +a remote host. + + +Managing an existing cluster +============================ + +You can use ceph-deploy to provision nodes for an existing cluster. +To grab a copy of the cluster configuration file (normally +``ceph.conf``):: + + ceph-deploy config pull HOST + +You will usually also want to gather the encryption keys used for that +cluster:: + + ceph-deploy gatherkeys MONHOST + +At this point you can skip the steps below that create a new cluster +(you already have one) and optionally skip installation and/or monitor +creation, depending on what you are trying to accomplish. + + +Installing packages +=================== +For detailed information on installation instructions refer to the :ref:`install` +section. + +Proxy or Firewall Installs +-------------------------- +If attempting to install behind a firewall or through a proxy you can +use the ``--no-adjust-repos`` that will tell ceph-deploy to skip any changes +to the distro's repository in order to install the packages and it will go +straight to package installation. + +That will allow an environment without internet access to point to *its own +repositories*. This means that those repositories will need to be properly +setup (and mirrored with all the necessary dependencies) before attempting an +install. + +Another alternative is to set the `wget` env variables to point to the right +hosts, for example:: + + http_proxy=http://host:port + ftp_proxy=http://host:port + https_proxy=http://host:port + + +Creating a new configuration +============================ + +To create a new configuration file and secret key, decide what hosts +will run ``ceph-mon``, and run:: + + ceph-deploy new MON [MON..] + +For detailed information on new instructions refer to the :ref:`new` +section. + +For detailed information on ``new`` subcommand refer to the +:ref:`mon` section. + +Deploying monitors +================== + +To actually deploy ``ceph-mon`` to the hosts you chose, run:: + + ceph-deploy mon create HOST [HOST..] + +Without explicit hosts listed, hosts in ``mon_initial_members`` in the +config file are deployed. That is, the hosts you passed to +``ceph-deploy new`` are the default value here. + +For detailed information on ``mon`` subcommand refer to the +:ref:`mon` section. + +Gather keys +=========== + +To gather authentication keys (for administering the cluster and +bootstrapping new nodes) to the local directory, run:: + + ceph-deploy gatherkeys HOST [HOST...] + +where ``HOST`` is one of the monitor hosts. + +Once these keys are in the local directory, you can provision new OSDs etc. + +For detailed information on ``gatherkeys`` subcommand refer to the +:ref:`gatherkeys` section. + +Admin hosts +=========== + +To prepare a host with a ``ceph.conf`` and ``ceph.client.admin.keyring`` +keyring so that it can administer the cluster, run:: + + ceph-deploy admin HOST [HOST ...] + +Older versions of ceph-deploy automatically added the admin keyring to +all mon nodes making them admin nodes. For detailed information on the +admin command refer to the :ref:`admin` section. + +For detailed information on ``admin`` subcommand refer to the +:ref:`admin` section. + +Deploying OSDs +============== + +To create an OSD on a remote node, run:: + + ceph-deploy osd create HOST --data /path/to/device + +Alternatively, ``--data`` can accept a logical volume in the format of +``vg/lv`` + +After that, the hosts will be running OSDs for the given data disks or logical +volumes. For other OSD devices like journals (when using ``--filestore``) or +``block.db``, and ``block.wal``, these need to be logical volumes or GPT +partitions. + +.. note:: Partitions aren't created by this tool, they must be created + beforehand + + +Forget keys +=========== + +The ``new`` and ``gatherkeys`` put some Ceph authentication keys in keyrings in +the local directory. If you are worried about them being there for security +reasons, run:: + + ceph-deploy forgetkeys + +and they will be removed. If you need them again later to deploy additional +nodes, simply re-run:: + + ceph-deploy gatherkeys HOST [HOST...] + +and they will be retrieved from an existing monitor node. + +Multiple clusters +================= + +All of the above commands take a ``--cluster=NAME`` option, allowing +you to manage multiple clusters conveniently from one workstation. +For example:: + + ceph-deploy --cluster=us-west new + vi us-west.conf + ceph-deploy --cluster=us-west mon + +FAQ +=== + +Before anything +--------------- +Make sure you have the latest version of ``ceph-deploy``. It is actively +developed and releases are coming weekly (on average). The most recent versions +of ``ceph-deploy`` will have a ``--version`` flag you can use, otherwise check +with your package manager and update if there is anything new. + +Why is feature X not implemented? +--------------------------------- +Usually, features are added when/if it is sensible for someone that wants to +get started with ceph and said feature would make sense in that context. If +you believe this is the case and you've read "`what this tool is not`_" and +still think feature ``X`` should exist in ceph-deploy, open a feature request +in the ceph tracker: http://tracker.ceph.com/projects/ceph-deploy/issues + +A command gave me an error, what is going on? +--------------------------------------------- +Most of the commands for ``ceph-deploy`` are meant to be run remotely in a host +that you have configured when creating the initial config. If a given command +is not working as expected try to run the command that failed in the remote +host and assert the behavior there. + +If the behavior in the remote host is the same, then it is probably not +something wrong with ``ceph-deploy`` per-se. Make sure you capture the output +of both the ``ceph-deploy`` output and the output of the command in the remote +host. + +Issues with monitors +-------------------- +If your monitors are not starting, make sure that the ``{hostname}`` you used +when you ran ``ceph-deploy mon create {hostname}`` match the actual ``hostname -s`` +in the remote host. + +Newer versions of ``ceph-deploy`` should warn you if the results are different +but that might prevent the monitors from reaching quorum. diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 0000000..8291f88 --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,219 @@ + +.. _install: + +install +=========== +A few different distributions are supported with some flags to allow some +customization for installing ceph on remote nodes. + +Supported distributions: + +* Ubuntu +* Debian +* Fedora +* RedHat +* CentOS +* Suse +* Scientific Linux +* Arch Linux + +Before any action is taken, a platform detection call is done to make sure that +the platform that will get ceph installed is the correct one. If the platform +is not supported no further actions will proceed and an error message will be +displayed, similar to:: + + [ceph_deploy][ERROR ] UnsupportedPlatform: Platform is not supported: Mandriva + + +.. _install-stable-releases: + + +.. _note: + Although ceph-deploy installs some extra dependencies, do note that those + are not going to be uninstalled. For example librbd1 and librados which + qemu-kvm depends on, and removing it would cause issues for qemu-kvm. + +Distribution Notes +------------------ + +RPMs +^^^^ +On RPM-based distributions, ``yum-plugin-priorities`` is installed to make sure +that upstream ceph.com repos have a higher priority than distro repos. + +Because of packaging splits that are present in downstream repos that may not +be present in ceph.com repos, ``ceph-deploy`` enables the ``check_obsoletes`` +flag for the Yum priorities plugin. + +.. versionchanged:: 1.5.22 + Enable ``check_obsoletes`` by default + +RHEL +^^^^ +When installing packages on systems running Red Hat Enterprise Linux (RHEL), +``ceph-deploy`` will not install the latest upstream release by default. On other +distros, running ``ceph-deploy install`` without the ``--release`` flag will +install the latest upstream release by default (i.e. firefly, giant, etc). On +RHEL, the ``--release`` flag *must* be used if you wish to use the upstream +packages hosted on http://ceph.com. + +.. versionchanged:: 1.5.22 + Require ``--release`` flag to get upstream packages on RHEL + +Specific Releases +----------------- +By default the *latest* release is assumed. This value changes when +newer versions are available. If you are automating deployments it is better to +specify exactly what release you need:: + + ceph-deploy install --release emperor {host} + + +Note that the ``--stable`` flag for specifying a Ceph release is deprecated and +should no longer be used starting from version 1.3.6. + +.. versionadded:: 1.4.0 + +.. _install-unstable-releases: + +Unstable releases +----------------- +If you need to test cutting edge releases or a specific feature of ceph that +has yet to make it to a stable release you can specify this as well with +ceph-deploy with a couple of flags. + +To get the latest development release:: + + ceph-deploy install --testing {host} + +For a far more granular approach, you may want to specify a branch or a tag +from the repository, if none specified it fall backs to the latest commit in +master:: + + ceph-deploy install --dev {branch or tag} {host} + + +.. _install-behind-firewall: + +Behind Firewall +--------------- +For restrictive environments there are a couple of options to be able to +install ceph. + +If hosts have had some customizations with custom repositories and all is +needed is to proceed with a install of ceph, we can skip altering the source +repositories like:: + + ceph-deploy install --no-adjust-repos {host} + +Note that you will need to have working repositories that have all the +dependencies that ceph needs. In some distributions, other repos (besides the +ceph repos) will be added, like EPEL for CentOS. + +However, if there is a ceph repo mirror already set up you can point to it +before installation proceeds. For this specific action you will need two +arguments passed in (or optionally use environment variables). + +The repository URL and the GPG URL can be specified like this:: + + ceph-deploy install --repo-url {http mirror} --gpg-url {http gpg url} {host} + +Optionally, you can use the following environment variables: + +* ``CEPH_DEPLOY_REPO_URL`` +* ``CEPH_DEPLOY_GPG_URL`` + +Those values will be used to write to the ceph ``sources.list`` (in Debian and +Debian-based distros) or the ``yum.repos`` file for RPM distros and will skip +trying to compose the right URL for the release being installed. + +.. note:: + It is currently not possible to specify what version/release is to be + installed when ``--repo-url`` is used. + +It is strongly suggested that both flags be provided. However, the +``--gpg-url`` will default to the current one in the ceph repository:: + + https://download.ceph.com/keys/release.asc + +.. versionadded:: 1.3.3 + + +Local Mirrors +------------- +``ceph-deploy`` supports local mirror installation by syncing a repository to +remote servers and configuring correctly the remote hosts to install directly +from those local paths (as opposed to going through the network). + +The one requirement for this option to work is to have a ``release.asc`` at the +top of the directory that holds the repository files. + +That file is used by Ceph as the key for its signed packages and it is usually +retrieved from:: + + https://download.ceph.com/keys/release.asc + +This is how it would look the process to get Ceph installed from a local +repository in an admin host:: + + $ ceph-deploy install --local-mirror ~/tmp/rpm-mirror/ceph.com/rpm-emperor/el6 node2 + [ceph_deploy.cli][INFO ] Invoked (1.4.1): /bin/ceph-deploy install --local-mirror /Users/alfredo/tmp/rpm-mirror/ceph.com/rpm-emperor/el6 node2 + [ceph_deploy.install][DEBUG ] Installing stable version emperor on cluster ceph hosts node2 + [ceph_deploy.install][DEBUG ] Detecting platform for host node2 ... + [node2][DEBUG ] connected to host: node2 + [node2][DEBUG ] detect platform information from remote host + [node2][DEBUG ] detect machine type + [ceph_deploy.install][INFO ] Distro info: CentOS 6.4 Final + [node2][INFO ] installing ceph on node2 + [node2][INFO ] syncing file: noarch/ceph-deploy-1.3-0.noarch.rpm + [node2][INFO ] syncing file: noarch/ceph-deploy-1.3.1-0.noarch.rpm + [node2][INFO ] syncing file: noarch/ceph-deploy-1.3.2-0.noarch.rpm + [node2][INFO ] syncing file: noarch/ceph-release-1-0.el6.noarch.rpm + [node2][INFO ] syncing file: noarch/index.html + [node2][INFO ] syncing file: noarch/index.html?C=D;O=A + [node2][INFO ] syncing file: noarch/index.html?C=D;O=D + [node2][INFO ] syncing file: noarch/index.html?C=M;O=A + ... + [node2][DEBUG ] + [node2][DEBUG ] Installed: + [node2][DEBUG ] ceph.x86_64 0:0.72.1-0.el6 + [node2][DEBUG ] + [node2][DEBUG ] Complete! + [node2][INFO ] Running command: sudo ceph --version + [node2][DEBUG ] ceph version 0.72.1 + (4d923861868f6a15dcb33fef7f50f674997322de) + +.. versionadded:: 1.5.0 + + +Repo file only +-------------- +The ``install`` command has a flag that offers flexibility for installing +"repo files" only, avoiding installation of ceph and its dependencies. + +These "repo files" are the configuration files for package managers ("yum" or +"apt" for example) that point to the right repository information so that +certain packages become available. + +For APT these files would be `list files` and for YUM they would be `repo +files`. Regardless of the package manager, ceph-deploy is able to install this +file correctly so that the Ceph packages are available. This is useful in +a situation where a massive upgrade is needed and ``ceph-deploy`` would be too +slow to install sequentially in every host. + +Repositories are specified in the ``cephdeploy.conf`` (or +``$HOME/.cephdeploy.conf``) file. If a specific repository section is needed, +it can be specified with the ``--release`` flag:: + + ceph-deploy install --repo --release firefly {HOSTS} + +The above command would install the ``firefly`` repo file in every ``{HOST}`` +specified. + +If a repository section exists with the ``default = True`` flag, there is no +need to specify anything else and the repo file can be installed simply by +passing in the hosts:: + + ceph-deploy install --repo {HOSTS} + +.. versionadded:: 1.5.10 diff --git a/docs/source/mds.rst b/docs/source/mds.rst new file mode 100644 index 0000000..c7b1b10 --- /dev/null +++ b/docs/source/mds.rst @@ -0,0 +1,20 @@ +.. _mds: + +mds +======= +The ``mds`` subcommand provides an interface to interact with a cluster's +CephFS Metadata servers. + +create +---------- +Deploy MDS instances by specifying directly like:: + + ceph-deploy mds create node1 node2 node3 + +This will create an MDS on the given node(s) and start the +corresponding service. + +The MDS instances will default to having a name corresponding to the hostname +where it runs. For example, ``mds.node1``. + +.. note:: Removing MDS instances is not yet supported diff --git a/docs/source/mon.rst b/docs/source/mon.rst new file mode 100644 index 0000000..7942500 --- /dev/null +++ b/docs/source/mon.rst @@ -0,0 +1,106 @@ +.. _mon: + +mon +======= +The ``mon`` subcommand provides an interface to interact with a cluster's +monitors. The tool makes a few assumptions that are needed to implement the +most common scenarios. Monitors are usually very particular in what they need +to work correctly. + +.. note:: Before version v1.5.33 ceph-deploy relied upon ``ceph-create-keys``. + Using ``ceph-create-keys`` produced a side effect of deploying all + bootstrap keys on the mon node so making all mon nodes admin nodes. + This can be recreated by running the admin command on all mon nodes + see :ref:`admin` section. + +create-initial +------------------ +Will deploy for monitors defined in ``mon initial members``, wait until +they form quorum and then ``gatherkeys``, reporting the monitor status along +the process. If monitors don't form quorum the command will eventually +time out. + +This is the *preferred* way of initially deploying monitors since it will +compound a few of the steps needed together while looking for possible issues +along the way. + +:: + + ceph-deploy mon create-initial + + +create +---------- +Deploy monitors by specifying directly like:: + + ceph-deploy mon create node1 node2 node3 + +If no hosts are passed it will default to use the `mon initial members` +defined in the configuration. + +Please note that if this is an initial monitor deployment, the preferred way +is to use ``create-initial``. + + +add +------- +Add a monitor to an existing cluster:: + + ceph-deploy mon add node1 + +Since monitor hosts can have different network interfaces, this command allows +you to specify the interface IP in a few different ways. + +**``--address``**: this will explicitly override any configured address for +that host. Usage:: + + ceph-deploy mon add node1 --address 192.168.1.10 + + +**ceph.conf**: If a section for the node that is being added exists and it +defines a ``mon addr`` key. For example:: + + [mon.node1] + mon addr = 192.168.1.10 + +**resolving/dns**: if the monitor address is not defined in the configuration file +nor overridden in the command-line it will fall-back to resolving the address +of the provided host. + +.. warning:: If the monitor host has multiple addresses you should specify + the address directly to ensure the right IP is used. Please + note, only one node can be added at a time. + +.. versionadded:: 1.4.0 + + +destroy +----------- +Completely remove monitors on a remote host. Requires hostname(s) as +arguments:: + + ceph-deploy mon destroy node1 node2 node3 + + +--keyrings +-------------- +Both ``create`` and ``create-initial`` subcommands can be used with the +``--keyrings`` flag that accepts a path to search for keyring files. + +When this flag is used it will then look into the passed in path for files that +end with ``.keyring`` and will proceed to concatenate them in memory and seed +them to the monitor being created in the remote mode. + +This is useful when having several different keyring files that are needed at +initial setup, but normally, ceph-deploy will only use the +``$cluster.mon.keyring`` file for initial seeding. + +To keep things in order, create a directory and use that directory to store all +the keyring files that are needed. This is how the commands would look like for +a directory called ``keyrings``:: + + ceph-deploy mon --keyrings keyrings create-initial + +Or for the ``create`` sub-command:: + + ceph-deploy mon --keyrings keyrings create {nodes} diff --git a/docs/source/new.rst b/docs/source/new.rst new file mode 100644 index 0000000..e71d4dd --- /dev/null +++ b/docs/source/new.rst @@ -0,0 +1,75 @@ +.. _new: + +new +======= +This subcommand is used to generate a working ``ceph.conf`` file that will +contain important information for provisioning nodes and/or adding them to +a cluster. + + +SSH Keys +-------- +Ideally, all nodes will be pre-configured to have their passwordless access +from the machine executing ``ceph-deploy`` but you can also take advantage of +automatic detection of this when calling the ``new`` subcommand. + +Once called, it will try to establish an SSH connection to the hosts passed +into the ``new`` subcommand, and determine if it can (or cannot) connect +without a password prompt. + +If it can't proceed, it will try to copy *existing* keys to the remote host, if +those do not exist, then passwordless ``rsa`` keys will be generated for the +current user and those will get used. + +This feature can be overridden in the ``new`` subcommand like:: + + ceph-deploy new --no-ssh-copykey + +.. versionadded:: 1.3.2 + + +Creating a new configuration +---------------------------- + +To create a new configuration file and secret key, decide what hosts +will run ``ceph-mon``, and run:: + + ceph-deploy new MON [MON..] + +listing the hostnames of the monitors. Each ``MON`` can be + + * a simple hostname. It must be DNS resolvable without the fully + qualified domain name. + * a fully qualified domain name. The hostname is assumed to be the + leading component up to the first ``.``. + * a ``HOST:FQDN`` pair, of both the hostname and a fully qualified + domain name or IP address. For example, ``foo``, + ``foo.example.com``, ``foo:something.example.com``, and + ``foo:1.2.3.4`` are all valid. Note, however, that the hostname + should match that configured on the host ``foo``. + +The above will create a ``ceph.conf`` and ``ceph.mon.keyring`` in your +current directory. + + +Edit initial cluster configuration +---------------------------------- + +You want to review the generated ``ceph.conf`` file and make sure that +the ``mon_host`` setting contains the IP addresses you would like the +monitors to bind to. These are the IPs that clients will initially +contact to authenticate to the cluster, and they need to be reachable +both by external client-facing hosts and internal cluster daemons. + + +--cluster-network --public-network +---------------------------------- +Are used to provide subnets so that nodes can communicate within that +network. If passed, validation will occur by looking at the remote IP addresses +and making sure that at least one of those addresses is valid for the given +subnet. + +Those values will also be added to the generated ``ceph.conf``. If IPs are not +correct (or not in the subnets specified) an error will be raised. + +.. versionadded:: 1.5.13 diff --git a/docs/source/pkg.rst b/docs/source/pkg.rst new file mode 100644 index 0000000..2f70d08 --- /dev/null +++ b/docs/source/pkg.rst @@ -0,0 +1,58 @@ + +.. _pkg: + +pkg +======= +Provides a simple interface to install or remove packages on a remote host (or +a number of remote hosts). + +Packages to install or remove *must* be comma separated when there are more +than one package in the argument. + +.. note:: + This feature only supports installing on same distributions. You cannot + install a given package on different distributions at the same time. + + +.. _pkg-install: + +--install +------------- +This flag will use the package (or packages) passed in to perform an installation using +the distribution package manager in a non-interactive way. Package managers +that tend to ask for confirmation will not prompt. + +An example call to install a few packages on 2 hosts (with hostnames like +``node1`` and ``node2``) would look like:: + + ceph-deploy pkg --install vim,zsh node1 node2 + [ceph_deploy.cli][INFO ] Invoked (1.3.3): /bin/ceph-deploy pkg --install vim,zsh node1 node2 + [node1][DEBUG ] connected to host: node1 + [node1][DEBUG ] detect platform information from remote host + [node1][DEBUG ] detect machine type + [ceph_deploy.pkg][INFO ] Distro info: Ubuntu 12.04 precise + [node1][INFO ] installing packages on node1 + [node1][INFO ] Running command: sudo env DEBIAN_FRONTEND=noninteractive apt-get -q install --assume-yes vim zsh + ... + + +.. _pkg-remove: + +--remove +------------ +This flag will use the package (or packages) passed in to remove them using +the distribution package manager in a non-interactive way. Package managers +that tend to ask for confirmation will not prompt. + +An example call to remove a few packages on 2 hosts (with hostnames like +``node1`` and ``node2``) would look like:: + + + [ceph_deploy.cli][INFO ] Invoked (1.3.3): /bin/ceph-deploy pkg --remove vim,zsh node1 node2 + [node1][DEBUG ] connected to host: node1 + [node1][DEBUG ] detect platform information from remote host + [node1][DEBUG ] detect machine type + [ceph_deploy.pkg][INFO ] Distro info: Ubuntu 12.04 precise + [node1][INFO ] removing packages from node1 + [node1][INFO ] Running command: sudo apt-get -q remove -f -y --force-yes -- vim zsh + ... diff --git a/docs/source/repo.rst b/docs/source/repo.rst new file mode 100644 index 0000000..e862048 --- /dev/null +++ b/docs/source/repo.rst @@ -0,0 +1,77 @@ +.. _repo: + +repo +===== +Provides a simple interface for installing or removing new Apt or RPM repo files. + +Apt repo files are added in ``/etc/apt/sources.list.d``, while RPM repo files +are added in ``/etc/yum.repos.d``. + +.. _repo-install: + +Installing repos +---------------- + +Repos can be defined through CLI arguments, or they can be defined in cephdeploy.conf +and referenced by name. + +The general format for adding a repo is:: + + ceph-deploy repo --repo-url --gpg-url [host [host ...]] + +As an example of adding the Ceph rpm-hammer repo for EL7:: + + ceph-deploy repo --repo-url http://ceph.com/rpm-hammer/el7/x86_64/ --gpg-url 'https://download.ceph.com/keys/release.asc' ceph HOST1 + +In this example, the repo-name is ``ceph``, and the file ``/etc/yum.repos.d/ceph.repo`` +will be created. Because ``--gpg-url`` was passed, the repo will have ``gpgcheck=1`` +and will reference the given GPG key. + +For APT, the equivalent example would be:: + + ceph-deploy repo --repo-url http://ceph.com/debian-hammer --gpg-url 'https://download.ceph.com/keys/release.asc' ceph HOST1 + +If a repo was defined in cephdeploy.conf, like the following:: + + [ceph-mon] + name=Ceph-MON + baseurl=https://cephmirror.com/hammer/el7/x86_64 + gpgkey=https://cephmirror.com/release.asc + gpgcheck=1 + proxy=_none_ + +This could be installed with this command:: + + ceph-deploy repo ceph-mon HOST1 + +``ceph-deploy repo`` will always check to see if a matching repo name exists in +cephdeploy.conf first. + +It is possible that repos may be password protected, and a URL may be structured like so:: + + https://:@host.com/... + +In this case, Apt repositories will be created with mode ``0600`` to make +sure the password is not world-readable. You can also use the +``CEPH_DEPLOY_REPO_URL`` and ``CEPH_DEPLOY_GPG_URL`` environment variables in lieu +of ``--repo-url`` and ``--gpg-url`` to avoid placing sensitive credentials on the +command line (and thus visible in the process table). + +.. note:: + The writing of a repo file as mode 0600 when a password is present is only done + for Apt repos currently. + +.. _repo-remove: + +Removing +-------- + +Repos are simply removed by name. The general format for adding a repo is:: + + ceph-deploy repo --remove [host [host...]] + +To remove a repo at ``/etc/yum.repos.d/ceph.repo``, do:: + + ceph-deploy repo --remove ceph HOST1 + +.. versionadded:: 1.5.27 diff --git a/docs/source/rgw.rst b/docs/source/rgw.rst new file mode 100644 index 0000000..4a060c5 --- /dev/null +++ b/docs/source/rgw.rst @@ -0,0 +1,36 @@ +.. _rgw: + +rgw +======= +The ``rgw`` subcommand provides an interface to interact with a cluster's +RADOS Gateway instances. + +create +---------- +Deploy RGW instances by specifying directly like:: + + ceph-deploy rgw create node1 node2 node3 + +This will create an instance of RGW on the given node(s) and start the +corresponding service. The daemon will listen on the default port of 7480. + +The RGW instances will default to having a name corresponding to the hostname +where it runs. For example, ``rgw.node1``. + +If a custom name is desired for the RGW daemon, it can be specific like:: + + ceph-deploy rgw create node1:foo + +Custom names are automatically prefixed with "rgw.", so the resulting daemon +name would be "rgw.foo". + +.. note:: If an error is presented about the ``bootstrap-rgw`` keyring not being + found, that is because the ``bootstrap-rgw`` only been auto-created on + new clusters starting with the Hammer release. + +.. versionadded:: 1.5.23 + +.. note:: Removing RGW instances is not yet supported + +.. note:: Changing the port on which RGW will listen at deployment time is not yet + supported. diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..7d7303e --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest >=2.1.3 +tox >=1.2 +mock >=1.0.1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ + diff --git a/scripts/build-debian.sh b/scripts/build-debian.sh new file mode 100755 index 0000000..5b59670 --- /dev/null +++ b/scripts/build-debian.sh @@ -0,0 +1,87 @@ +#! /bin/sh + +# Tag tree and update version number in change log and +# in setup.py before building. + +REPO=debian-repo +COMPONENT=main +KEYID=${KEYID:-03C3951A} # default is autobuild keyid +DEB_DIST="sid wheezy squeeze quantal precise oneiric natty raring" +DEB_BUILD=$(lsb_release -s -c) +RELEASE=0 + +if [ X"$1" = X"--release" ] ; then + echo "Release Build" + RELEASE=1 +fi + +if [ ! -d debian ] ; then + echo "Are we in the right directory" + exit 1 +fi + +if gpg --list-keys 2>/dev/null | grep -q ${KEYID} ; then + echo "Signing packages and repo with ${KEYID}" +else + echo "Package signing key (${KEYID}) not found" + echo "Have you set \$GNUPGHOME ? " + exit 3 +fi + +# Clean up any leftover builds +rm -f ../ceph-deploy*.dsc ../ceph-deploy*.changes ../ceph-deploy*.deb ../ceph-deploy.tgz +rm -rf ./debian-repo + +# Apply backport tag if release build +# I am going to jump out the window if this is not fixed and removed from the source +# of this package. There is absolutely **NO** reason why we need to hard code the +# DEBEMAIL like this. +if [ $RELEASE -eq 1 ] ; then + DEB_VERSION=$(dpkg-parsechangelog | sed -rne 's,^Version: (.*),\1, p') + BP_VERSION=${DEB_VERSION}${BPTAG} + DEBEMAIL="alfredo.deza@inktank.com" dch -D $DIST --force-distribution -b -v "$BP_VERSION" "$comment" + dpkg-source -b . +fi + +# Build Package +echo "Building for dist: $DEB_BUILD" +dpkg-buildpackage -k$KEYID +if [ $? -ne 0 ] ; then + echo "Build failed" + exit 2 +fi + +# Build Repo +PKG=../ceph-deploy*.changes +mkdir -p $REPO/conf +if [ -e $REPO/conf/distributions ] ; then + rm -f $REPO/conf/distributions +fi + +for DIST in $DEB_DIST ; do + cat <> $REPO/conf/distributions +Codename: $DIST +Suite: stable +Components: $COMPONENT +Architectures: amd64 armhf i386 source +Origin: Inktank +Description: Ceph distributed file system +DebIndices: Packages Release . .gz .bz2 +DscIndices: Sources Release .gz .bz2 +Contents: .gz .bz2 +SignWith: $KEYID + +EOF +done + +echo "Adding package to repo, dist: $DEB_BUILD ($PKG)" +reprepro --ask-passphrase -b $REPO -C $COMPONENT --ignore=undefinedtarget --ignore=wrongdistribution include $DEB_BUILD $PKG + +#for DIST in $DEB_DIST +#do +# [ "$DIST" = "$DEB_BUILD" ] && continue +# echo "Copying package to dist: $DIST" +# reprepro -b $REPO --ignore=undefinedtarget --ignore=wrongdistribution copy $DIST $DEB_BUILD ceph-deploy +#done + +echo "Done" diff --git a/scripts/build-rpm.sh b/scripts/build-rpm.sh new file mode 100755 index 0000000..9b330e4 --- /dev/null +++ b/scripts/build-rpm.sh @@ -0,0 +1,59 @@ +#! /bin/sh + +# Tag tree and update version number in change log and +# in setup.py before building. + +REPO=rpm-repo +KEYID=${KEYID:-03C3951A} # Default is autobuild-key +BUILDAREA=./rpmbuild +DIST=el6 +RPM_BUILD=$(lsb_release -s -c) + +if [ ! -e setup.py ] ; then + echo "Are we in the right directory" + exit 1 +fi + +if gpg --list-keys 2>/dev/null | grep -q ${KEYID} ; then + echo "Signing packages and repo with ${KEYID}" +else + echo "Package signing key (${KEYID}) not found" + echo "Have you set \$GNUPGHOME ? " + exit 3 +fi + +if ! CREATEREPO=`which createrepo` ; then + echo "Please install the createrepo package" + exit 4 +fi + +# Create Tarball +python setup.py sdist --formats=bztar + +# Build RPM +mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} +BUILDAREA=`readlink -fn ${BUILDAREA}` ### rpm wants absolute path +cp ceph-deploy.spec ${BUILDAREA}/SPECS +cp dist/*.tar.bz2 ${BUILDAREA}/SOURCES +echo "buildarea is: ${BUILDAREA}" +rpmbuild -ba --define "_topdir ${BUILDAREA}" --define "_unpackaged_files_terminate_build 0" ${BUILDAREA}/SPECS/ceph-deploy.spec + +# create repo +DEST=${REPO}/${DIST} +mkdir -p ${REPO}/${DIST} +cp -r ${BUILDAREA}/*RPMS ${DEST} + +# Sign all the RPMs for this release +rpm_list=`find ${REPO} -name "*.rpm" -print` +rpm --addsign --define "_gpg_name ${KEYID}" $rpm_list + +# Construct repodata +for dir in ${DEST}/SRPMS ${DEST}/RPMS/* +do + if [ -d $dir ] ; then + createrepo $dir + gpg --detach-sign --armor -u ${KEYID} $dir/repodata/repomd.xml + fi +done + +exit 0 diff --git a/scripts/ceph-deploy b/scripts/ceph-deploy new file mode 100755 index 0000000..cc8dd62 --- /dev/null +++ b/scripts/ceph-deploy @@ -0,0 +1,21 @@ +#!/usr/bin/env python +import os +import platform +import sys +""" +ceph-deploy - admin tool for ceph +""" + +if os.path.exists('/usr/share/pyshared/ceph_deploy'): + sys.path.insert(0,'/usr/share/pyshared/ceph_deploy') +elif os.path.exists('/usr/share/ceph-deploy'): + sys.path.insert(0,'/usr/share/ceph-deploy') +elif os.path.exists('/usr/share/pyshared/ceph-deploy'): + sys.path.insert(0,'/usr/share/pyshared/ceph-deploy') +elif os.path.exists('/usr/lib/python2.6/site-packages/ceph_deploy'): + sys.path.insert(0,'/usr/lib/python2.6/site-packages/ceph_deploy') + +from ceph_deploy.cli import main + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/jenkins-build b/scripts/jenkins-build new file mode 100755 index 0000000..9d0d212 --- /dev/null +++ b/scripts/jenkins-build @@ -0,0 +1,53 @@ +#!/bin/sh + +# This is the script that runs inside Jenkins. +# http://jenkins.ceph.com/job/ceph-deploy/ + +set -x +set -e + +# Jenkins will set $RELEASE as a parameter in the job configuration. +if $RELEASE ; then + # This is a formal release. Sign it with the release key. + export GNUPGHOME=/home/jenkins-build/build/gnupg.ceph-release/ + export KEYID=17ED316D +else + # This is an automatic build. Sign it with the autobuild key. + export GNUPGHOME=/home/jenkins-build/build/gnupg.autobuild/ + export KEYID=03C3951A +fi + +HOST=$(hostname --short) +echo "Building on ${HOST}" +echo " DIST=${DIST}" +echo " BPTAG=${BPTAG}" +echo " KEYID=${KEYID}" +echo " WS=$WORKSPACE" +echo " PWD=$(pwd)" +echo " BRANCH=$BRANCH" + +case $HOST in +gitbuilder-*-rpm*) + rm -rf rpm-repo dist/* build/rpmbuild + ./scripts/build-rpm.sh --release + if [ $? -eq 0 ] ; then + cd $WORKSPACE + mkdir -p dist + fi + ;; +gitbuilder-cdep-deb* | tala* | mira*) + rm -rf debian-repo + rm -rf dist + rm -f ../*.changes ../*.dsc ../*.gz ../*.diff + ./scripts/build-debian.sh --release + if [ $? -eq 0 ] ; then + cd $WORKSPACE + mkdir -p dist + mv ../*.changes ../*.dsc ../*.deb ../*.tar.gz dist/. + fi + ;; +*) + echo "Can't determine build host type" + exit 4 + ;; +esac diff --git a/scripts/jenkins-pull-requests-build b/scripts/jenkins-pull-requests-build new file mode 100644 index 0000000..24f2867 --- /dev/null +++ b/scripts/jenkins-pull-requests-build @@ -0,0 +1,14 @@ +#!/bin/sh + +set -x +set -e + +# This is the script that runs inside Jenkins for each pull request. +# http://jenkins.ceph.com/job/ceph-deploy-pull-requests/ + +virtualenv --version +virtualenv venv +. venv/bin/activate +#venv/bin/python setup.py develop +venv/bin/pip install tox +venv/bin/tox -rv diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b9f9db4 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[tool:pytest] +norecursedirs = .* _* virtualenv diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..09b52f1 --- /dev/null +++ b/setup.py @@ -0,0 +1,74 @@ +from setuptools import setup, find_packages +import os +import sys +import ceph_deploy +import pkg_resources +import setuptools + +def read(fname): + path = os.path.join(os.path.dirname(__file__), fname) + f = open(path) + return f.read() + + +if (pkg_resources.parse_version(setuptools.__version__) >= + pkg_resources.parse_version('36.2.0')): + install_requires = [ + "remoto >= 1.1.4", + "configparser;python_version<'3.0'", + "setuptools < 45.0.0;python_version<'3.0'", + "setuptools;python_version>='3.0'"] +else: + install_requires = [ + "remoto >= 1.1.4", + "configparser", + "setuptools < 45.0.0"] + +setup( + name='ceph-deploy', + version=ceph_deploy.__version__, + packages=find_packages(), + + author='Inktank', + author_email='ceph-devel@vger.kernel.org', + description='Deploy Ceph with minimal infrastructure', + long_description=read('README.rst'), + license='MIT', + keywords='ceph deploy', + url="https://github.com/ceph/ceph-deploy", + + install_requires=install_requires, + + tests_require=[ + 'pytest >=2.1.3', + 'mock >=1.0b1', + ], + + entry_points={ + + 'console_scripts': [ + 'ceph-deploy = ceph_deploy.cli:main', + ], + + 'ceph_deploy.cli': [ + 'new = ceph_deploy.new:make', + 'install = ceph_deploy.install:make', + 'uninstall = ceph_deploy.install:make_uninstall', + 'purge = ceph_deploy.install:make_purge', + 'purgedata = ceph_deploy.install:make_purge_data', + 'mon = ceph_deploy.mon:make', + 'gatherkeys = ceph_deploy.gatherkeys:make', + 'osd = ceph_deploy.osd:make', + 'disk = ceph_deploy.osd:make_disk', + 'mds = ceph_deploy.mds:make', + 'mgr = ceph_deploy.mgr:make', + 'forgetkeys = ceph_deploy.forgetkeys:make', + 'config = ceph_deploy.config:make', + 'admin = ceph_deploy.admin:make', + 'pkg = ceph_deploy.pkg:make', + 'rgw = ceph_deploy.rgw:make', + 'repo = ceph_deploy.repo:make', + ], + + }, + ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a2722c3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,28 @@ +[tox] +envlist = py27, py35, py36, flake8 +skip_missing_interpreters = true + +[testenv] +deps= + pytest + mock==1.0.1 +setenv = + CEPH_DEPLOY_TEST = 1 +commands=py.test -v {posargs:ceph_deploy/tests} + +[testenv:docs] +basepython=python +changedir=docs/source +deps=sphinx +commands= + sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html + +[testenv:flake8] +deps=flake8 +commands=flake8 --select=F,E9 --exclude=vendor {posargs:ceph_deploy} + +# Note that ``remoto`` is not added as a dependency here as it is assumed +# that the tester will have the distro version of remoto installed +[testenv:py27-novendor] +sitepackages=True +deps= diff --git a/vendor.py b/vendor.py new file mode 100644 index 0000000..9d49efa --- /dev/null +++ b/vendor.py @@ -0,0 +1,112 @@ +from __future__ import print_function + +import subprocess +import os +from os import path +import re +import traceback +import sys + + +error_msg = """ +This library depends on sources fetched when packaging that failed to be +retrieved. + +This means that it will *not* work as expected. Errors encountered: +""" + + +def run(cmd): + print('[vendoring] Running command: %s' % ' '.join(cmd)) + try: + result = subprocess.Popen( + cmd, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE + ) + except Exception: + # if building with python2.5 this makes it compatible + _, error, _ = sys.exc_info() + print_error([], traceback.format_exc(error).split('\n')) + raise SystemExit(1) + + if result.wait(): + print_error(result.stdout.readlines(), result.stderr.readlines()) + + return result.returncode + + +def print_error(stdout, stderr): + print('*'*80) + print(error_msg) + for line in stdout: + print(line) + for line in stderr: + print(line) + print('*'*80) + + +def vendor_library(name, version, cmd=None): + this_dir = path.dirname(path.abspath(__file__)) + vendor_dest = path.join(this_dir, 'ceph_deploy/lib/vendor/%s' % name) + vendor_init = path.join(vendor_dest, '__init__.py') + vendor_src = path.join(this_dir, name) + vendor_module = path.join(vendor_src, name) + current_dir = os.getcwd() + + if path.exists(vendor_src): + run(['rm', '-rf', vendor_src]) + + if path.exists(vendor_init): + # The following read/regex is done so that we can parse module metadata without the need + # to import it. Module metadata is defined as variables with double underscores. We are + # particularly insteresting in the version string, so we look into single or double quoted + # values, like: __version__ = '1.0' + module_file = open(vendor_init).read() + metadata = dict(re.findall(r"__([a-z]+)__\s*=\s*['\"]([^'\"]*)['\"]", module_file)) + if metadata.get('version') != version: + run(['rm', '-rf', vendor_dest]) + + if not path.exists(vendor_dest): + rc = run(['git', 'clone', 'git://git.ceph.com/%s' % name]) + if rc: + print("%s: git clone failed using ceph.com url with rc %s, trying github.com" % (path.basename(__file__), rc)) + run(['git', 'clone', 'https://github.com/ceph/%s.git' % name]) + os.chdir(vendor_src) + run(['git', 'checkout', version]) + if cmd: + run(cmd) + run(['mv', vendor_module, vendor_dest]) + os.chdir(current_dir) + + +def clean_vendor(name): + """ + Ensure that vendored code/dirs are removed, possibly when packaging when + the environment flag is set to avoid vendoring. + """ + this_dir = path.dirname(path.abspath(__file__)) + vendor_dest = path.join(this_dir, 'ceph_deploy/lib/vendor/%s' % name) + run(['rm', '-rf', vendor_dest]) + + +def vendorize(vendor_requirements): + """ + This is the main entry point for vendorizing requirements. It expects + a list of tuples that should contain the name of the library and the + version. + + For example, a library ``foo`` with version ``0.0.1`` would look like:: + + vendor_requirements = [ + ('foo', '0.0.1'), + ] + """ + + for library in vendor_requirements: + if len(library) == 2: + name, version = library + cmd = None + elif len(library) == 3: # a possible cmd we need to run + name, version, cmd = library + vendor_library(name, version, cmd) -- 2.39.5