From afa87873b07400720ab4abbc0b48b03aff10aeea Mon Sep 17 00:00:00 2001 From: Travis Rhoden Date: Mon, 29 Jun 2015 14:32:55 -0700 Subject: [PATCH] [RM-12151] Add new 'ToggleRawTextHelpFormatter' The existing use of argparse.RawDescriptionHelpFormatter seems to stem from wanting to use newlines in argparse help output. To use the raw text formatter, you have to provide a static string that is always output with --help. The problem is that the defintion of this help string is pretty disconnected from the rest of the code. You are documenting options many lines away from where they are defined instead of normally being at the same time. To solve this, introduce a new ToggleRawTextHelpFormatter that allows for the optional use of Raw text on any individual help item by prefixing it with 'R|'. Signed-off-by: Travis Rhoden --- .gitignore | 22 + LICENSE | 19 + MANIFEST.in | 5 + README.rst | 373 +++++++++ bootstrap | 57 ++ ceph-deploy.spec | 74 ++ ceph_deploy/__init__.py | 3 + ceph_deploy/admin.py | 66 ++ ceph_deploy/calamari.py | 106 +++ ceph_deploy/cli.py | 192 +++++ ceph_deploy/cliutil.py | 8 + ceph_deploy/conf/__init__.py | 2 + ceph_deploy/conf/ceph.py | 96 +++ ceph_deploy/conf/cephdeploy.py | 215 +++++ ceph_deploy/config.py | 110 +++ ceph_deploy/connection.py | 44 + ceph_deploy/exc.py | 127 +++ ceph_deploy/forgetkeys.py | 36 + ceph_deploy/gatherkeys.py | 95 +++ ceph_deploy/hosts/__init__.py | 140 ++++ ceph_deploy/hosts/centos/__init__.py | 19 + ceph_deploy/hosts/centos/install.py | 226 ++++++ ceph_deploy/hosts/centos/mon/__init__.py | 2 + ceph_deploy/hosts/centos/mon/create.py | 24 + ceph_deploy/hosts/centos/pkg.py | 15 + ceph_deploy/hosts/centos/uninstall.py | 17 + ceph_deploy/hosts/common.py | 163 ++++ ceph_deploy/hosts/debian/__init__.py | 21 + ceph_deploy/hosts/debian/install.py | 217 +++++ ceph_deploy/hosts/debian/mon/__init__.py | 2 + ceph_deploy/hosts/debian/mon/create.py | 42 + ceph_deploy/hosts/debian/pkg.py | 15 + ceph_deploy/hosts/debian/uninstall.py | 16 + ceph_deploy/hosts/fedora/__init__.py | 20 + ceph_deploy/hosts/fedora/install.py | 88 ++ ceph_deploy/hosts/fedora/mon/__init__.py | 2 + ceph_deploy/hosts/fedora/mon/create.py | 21 + ceph_deploy/hosts/fedora/uninstall.py | 15 + ceph_deploy/hosts/remotes.py | 343 ++++++++ ceph_deploy/hosts/rhel/__init__.py | 19 + ceph_deploy/hosts/rhel/install.py | 85 ++ ceph_deploy/hosts/rhel/mon/__init__.py | 2 + ceph_deploy/hosts/rhel/mon/create.py | 24 + ceph_deploy/hosts/rhel/pkg.py | 15 + ceph_deploy/hosts/rhel/uninstall.py | 18 + ceph_deploy/hosts/suse/__init__.py | 26 + ceph_deploy/hosts/suse/install.py | 189 +++++ ceph_deploy/hosts/suse/mon/__init__.py | 2 + ceph_deploy/hosts/suse/mon/create.py | 19 + ceph_deploy/hosts/suse/pkg.py | 15 + ceph_deploy/hosts/suse/uninstall.py | 20 + ceph_deploy/hosts/util.py | 31 + ceph_deploy/install.py | 633 +++++++++++++++ ceph_deploy/lib/__init__.py | 27 + ceph_deploy/lib/vendor/__init__.py | 0 ceph_deploy/mds.py | 229 ++++++ ceph_deploy/misc.py | 22 + ceph_deploy/mon.py | 598 ++++++++++++++ ceph_deploy/new.py | 279 +++++++ ceph_deploy/osd.py | 751 ++++++++++++++++++ ceph_deploy/pkg.py | 74 ++ ceph_deploy/rgw.py | 208 +++++ ceph_deploy/tests/__init__.py | 0 ceph_deploy/tests/conftest.py | 98 +++ ceph_deploy/tests/directory.py | 13 + ceph_deploy/tests/fakes.py | 27 + ceph_deploy/tests/parser/__init__.py | 0 ceph_deploy/tests/parser/test_admin.py | 32 + ceph_deploy/tests/parser/test_calamari.py | 48 ++ ceph_deploy/tests/parser/test_config.py | 61 ++ ceph_deploy/tests/parser/test_disk.py | 166 ++++ ceph_deploy/tests/parser/test_gatherkeys.py | 32 + ceph_deploy/tests/parser/test_install.py | 159 ++++ ceph_deploy/tests/parser/test_main.py | 109 +++ ceph_deploy/tests/parser/test_mds.py | 35 + ceph_deploy/tests/parser/test_mon.py | 128 +++ ceph_deploy/tests/parser/test_new.py | 83 ++ ceph_deploy/tests/parser/test_osd.py | 207 +++++ ceph_deploy/tests/parser/test_purge.py | 32 + ceph_deploy/tests/parser/test_purgedata.py | 32 + ceph_deploy/tests/parser/test_rgw.py | 35 + ceph_deploy/tests/parser/test_uninstall.py | 32 + ceph_deploy/tests/test_cli_admin.py | 60 ++ ceph_deploy/tests/test_cli_mon.py | 78 ++ ceph_deploy/tests/test_cli_new.py | 71 ++ ceph_deploy/tests/test_cli_rgw.py | 11 + ceph_deploy/tests/test_conf.py | 68 ++ ceph_deploy/tests/test_install.py | 113 +++ ceph_deploy/tests/test_mon.py | 92 +++ ceph_deploy/tests/test_remotes.py | 85 ++ ceph_deploy/tests/unit/hosts/test_centos.py | 64 ++ ceph_deploy/tests/unit/hosts/test_hosts.py | 409 ++++++++++ ceph_deploy/tests/unit/hosts/test_remotes.py | 16 + ceph_deploy/tests/unit/hosts/test_suse.py | 25 + ceph_deploy/tests/unit/hosts/test_util.py | 29 + ceph_deploy/tests/unit/test_calamari.py | 17 + 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 | 225 ++++++ ceph_deploy/tests/unit/test_new.py | 28 + ceph_deploy/tests/unit/test_osd.py | 74 ++ .../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 | 32 + ceph_deploy/tests/unit/util/test_paths.py | 50 ++ .../tests/unit/util/test_pkg_managers.py | 139 ++++ ceph_deploy/tests/unit/util/test_system.py | 19 + ceph_deploy/tests/unit/util/test_templates.py | 29 + ceph_deploy/tests/util.py | 28 + ceph_deploy/util/__init__.py | 0 ceph_deploy/util/arg_validators.py | 83 ++ ceph_deploy/util/constants.py | 32 + ceph_deploy/util/decorators.py | 112 +++ ceph_deploy/util/files.py | 5 + ceph_deploy/util/help_formatters.py | 11 + ceph_deploy/util/log.py | 67 ++ ceph_deploy/util/net.py | 363 +++++++++ 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 | 166 ++++ ceph_deploy/util/ssh.py | 32 + ceph_deploy/util/system.py | 58 ++ ceph_deploy/util/templates.py | 95 +++ ceph_deploy/validate.py | 16 + debian/ceph-deploy.install | 1 + debian/changelog | 275 +++++++ debian/compat | 1 + debian/control | 25 + 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/changelog.rst | 401 ++++++++++ docs/source/conf.py | 268 +++++++ docs/source/conf.rst | 175 ++++ docs/source/contents.rst | 15 + docs/source/index.rst | 298 +++++++ docs/source/install.rst | 218 +++++ docs/source/mds.rst | 20 + docs/source/mon.rst | 100 +++ docs/source/new.rst | 75 ++ docs/source/pkg.rst | 58 ++ 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 | 79 ++ tox.ini | 32 + vendor.py | 110 +++ 168 files changed, 13932 insertions(+) create mode 100644 .gitignore 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/calamari.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/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/mon/create.py create mode 100644 ceph_deploy/hosts/centos/pkg.py create mode 100644 ceph_deploy/hosts/centos/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/mon/create.py create mode 100644 ceph_deploy/hosts/debian/pkg.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/mon/create.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/mon/create.py create mode 100644 ceph_deploy/hosts/rhel/pkg.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/mon/create.py create mode 100644 ceph_deploy/hosts/suse/pkg.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/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/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_calamari.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_purge.py create mode 100644 ceph_deploy/tests/parser/test_purgedata.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_install.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_centos.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_calamari.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/test_osd.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_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/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/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/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/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/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/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..85aec15 --- /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 http://ceph.com/ceph-deploy/docs + +.. _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..9b6fda8 --- /dev/null +++ b/bootstrap @@ -0,0 +1,57 @@ +#!/bin/sh +set -e + +if command -v lsb_release >/dev/null 2>&1; then + case "$(lsb_release --id --short)" in + Ubuntu|Debian) + for package in python-virtualenv; 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 + ;; + esac + + case "$(lsb_release --id --short | awk '{print $1}')" in + openSUSE|SUSE) + 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 zypper install $missing" + exit 1 + fi + ;; + esac + +else + 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 + ;; + esac + fi +fi + +test -d virtualenv || virtualenv virtualenv +./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..5678d26 --- /dev/null +++ b/ceph-deploy.spec @@ -0,0 +1,74 @@ +# +# spec file for package ceph-deploy +# + +%if ! (0%{?fedora} > 12 || 0%{?rhel} > 5) +%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} +%{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")} +%endif + +################################################################################# +# common +################################################################################# +Name: ceph-deploy +Version: 1.5.25 +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 +BuildRequires: python-devel +BuildRequires: python-distribute +BuildRequires: python-setuptools +BuildRequires: python-virtualenv +BuildRequires: python-mock +BuildRequires: python-tox +%if 0%{?suse_version} +BuildRequires: python-pytest +%else +BuildRequires: pytest +%endif +BuildRequires: git +Requires: python-argparse +Requires: python-distribute +#Requires: lsb-release +#Requires: ceph +%if 0%{?suse_version} && 0%{?suse_version} <= 1110 +%{!?python_sitelib: %global python_sitelib %(python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} +%else +BuildArch: noarch +%endif + +################################################################################# +# specific +################################################################################# +%if 0%{defined suse_version} +%py_requires +%endif + +%description +An easy to use admin tool for deploy ceph storage clusters. + +%prep +#%%setup -q -n %%{name} +%setup -q + +%build +#python setup.py build + +%install +python setup.py install --prefix=%{_prefix} --root=%{buildroot} +install -m 0755 -D scripts/ceph-deploy $RPM_BUILD_ROOT/usr/bin + +%clean +[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf "$RPM_BUILD_ROOT" + +%files +%defattr(-,root,root) +%doc LICENSE README.rst +%{_bindir}/ceph-deploy +%{python_sitelib}/* + +%changelog diff --git a/ceph_deploy/__init__.py b/ceph_deploy/__init__.py new file mode 100644 index 0000000..75d0b4e --- /dev/null +++ b/ceph_deploy/__init__.py @@ -0,0 +1,3 @@ + +__version__ = '1.5.25' + diff --git a/ceph_deploy/admin.py b/ceph_deploy/admin.py new file mode 100644 index 0000000..0d7ba8a --- /dev/null +++ b/ceph_deploy/admin.py @@ -0,0 +1,66 @@ +import logging + +from cStringIO import StringIO + +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): + cfg = conf.ceph.load(args) + conf_data = StringIO() + cfg.write(conf_data) + + try: + with file('%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.getvalue(), + args.overwrite_conf, + ) + + distro.conn.remote_module.write_file( + '/etc/ceph/%s.client.admin.keyring' % args.cluster, + keyring, + 0600, + ) + + 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/calamari.py b/ceph_deploy/calamari.py new file mode 100644 index 0000000..a6c63d6 --- /dev/null +++ b/ceph_deploy/calamari.py @@ -0,0 +1,106 @@ +import errno +import logging +import os +from ceph_deploy import hosts, exc +from ceph_deploy.lib import remoto + + +LOG = logging.getLogger(__name__) + + +def distro_is_supported(distro_name): + """ + An enforcer of supported distros that can differ from what ceph-deploy + supports. + """ + supported = ['centos', 'redhat', 'ubuntu', 'debian'] + if distro_name in supported: + return True + return False + + +def connect(args): + for hostname in args.hosts: + distro = hosts.get(hostname, username=args.username) + if not distro_is_supported(distro.normalized_name): + raise exc.UnsupportedPlatform( + distro.distro_name, + distro.codename, + distro.release + ) + + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + LOG.info('assuming that a repository with Calamari packages is already configured.') + LOG.info('Refer to the docs for examples (http://ceph.com/ceph-deploy/docs/conf.html)') + + rlogger = logging.getLogger(hostname) + + # Emplace minion config prior to installation so that it is present + # when the minion first starts. + minion_config_dir = os.path.join('/etc/salt/', 'minion.d') + minion_config_file = os.path.join(minion_config_dir, 'calamari.conf') + + rlogger.debug('creating config dir: %s' % minion_config_dir) + distro.conn.remote_module.makedir(minion_config_dir, [errno.EEXIST]) + + rlogger.debug( + 'creating the calamari salt config: %s' % minion_config_file + ) + distro.conn.remote_module.write_file( + minion_config_file, + 'master: %s\n' % args.master + ) + + distro.pkg.install(distro, 'salt-minion') + + # redhat/centos need to get the service started + if distro.normalized_name in ['redhat', 'centos']: + remoto.process.run( + distro.conn, + ['chkconfig', 'salt-minion', 'on'] + ) + + remoto.process.run( + distro.conn, + ['service', 'salt-minion', 'start'] + ) + + distro.conn.exit() + + +def calamari(args): + if args.subcommand == 'connect': + connect(args) + + +def make(parser): + """ + Install and configure Calamari nodes. Assumes that a repository with + Calamari packages is already configured. Refer to the docs for examples + (http://ceph.com/ceph-deploy/docs/conf.html) + """ + calamari_parser = parser.add_subparsers(dest='subcommand') + + calamari_connect = calamari_parser.add_parser( + 'connect', + help='Configure host(s) to connect to Calamari master' + ) + calamari_connect.add_argument( + '--master', + nargs='?', + metavar='MASTER SERVER', + help="The domain for the Calamari master server" + ) + calamari_connect.add_argument( + 'hosts', + nargs='+', + ) + + parser.set_defaults( + func=calamari, + ) diff --git a/ceph_deploy/cli.py b/ceph_deploy/cli.py new file mode 100644 index 0000000..b07aa30 --- /dev/null +++ b/ceph_deploy/cli.py @@ -0,0 +1,192 @@ +import pkg_resources +import argparse +import logging +import textwrap +import os +import sys +from string import join + +import ceph_deploy +from ceph_deploy import exc, validate +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(): + parser = argparse.ArgumentParser( + prog='ceph-deploy', + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Easy Ceph deployment\n\n%s' % __header__, + ) + 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( + '--cluster', + metavar='NAME', + help='name of the cluster', + type=validate.alphanumeric, + ) + 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', + ) + 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(fn, 'priority', 100), + ) + for (name, fn) in entry_points: + p = sub.add_parser( + name, + description=fn.__doc__, + help=fn.__doc__, + ) + # ugly kludge but i really want to have a nice way to access + # the program name, with subcommand, later + p.set_defaults(prog=p.prog) + 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) + parser.set_defaults( + # we want to hold on to this, for later + prog=parser.prog, + 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('{cluster}.log'.format(cluster=args.cluster)) + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter(log.BASE_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..028d78d --- /dev/null +++ b/ceph_deploy/conf/__init__.py @@ -0,0 +1,2 @@ +import ceph # noqa +import cephdeploy # noqa diff --git a/ceph_deploy/conf/ceph.py b/ceph_deploy/conf/ceph.py new file mode 100644 index 0000000..9042923 --- /dev/null +++ b/ceph_deploy/conf/ceph.py @@ -0,0 +1,96 @@ +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') + + +class CephConf(ConfigParser.RawConfigParser): + 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.readfp(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 = file(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 file(path, 'rb') 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 file(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..30c991e --- /dev/null +++ b/ceph_deploy/conf/cephdeploy.py @@ -0,0 +1,215 @@ +from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError +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 = 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={gpgurl} + +# 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={gpgurl} +""".format(gpgurl=gpg.url('release')) + + +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(SafeConfigParser): + """ + Subclasses from SafeConfigParser 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 (NoSectionError, 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..75c3467 --- /dev/null +++ b/ceph_deploy/config.py @@ -0,0 +1,110 @@ +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 file(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 file(topath, 'w') 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): + """ + Push configuration file to a remote host. + """ + config_parser = parser.add_subparsers(dest='subcommand') + + 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..86bedbe --- /dev/null +++ b/ceph_deploy/forgetkeys.py @@ -0,0 +1,36 @@ +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', + ]: + try: + os.unlink('{cluster}.{what}.keyring'.format( + cluster=args.cluster, + what=f, + )) + except OSError, 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..d35a9ae --- /dev/null +++ b/ceph_deploy/gatherkeys.py @@ -0,0 +1,95 @@ +import os.path +import logging + +from ceph_deploy import hosts, exc +from ceph_deploy.cliutil import priority + + +LOG = logging.getLogger(__name__) + + +def fetch_file(args, frompath, topath, _hosts): + if os.path.exists(topath): + LOG.debug('Have %s', topath) + return True + else: + for hostname in _hosts: + filepath = frompath.format(hostname=hostname) + LOG.debug('Checking %s for %s', hostname, filepath) + distro = hosts.get(hostname, username=args.username) + key = distro.conn.remote_module.get_file(filepath) + + if key is not None: + LOG.debug('Got %s key from %s.', topath, hostname) + with file(topath, 'w') as f: + f.write(key) + return True + distro.conn.exit() + LOG.warning('Unable to find %s on %s', filepath, hostname) + return False + + +def gatherkeys(args): + oldmask = os.umask(077) + try: + # client.admin + keyring = '/etc/ceph/{cluster}.client.admin.keyring'.format( + cluster=args.cluster) + r = fetch_file( + args=args, + frompath=keyring, + topath='{cluster}.client.admin.keyring'.format( + cluster=args.cluster), + _hosts=args.mon, + ) + if not r: + raise exc.KeyNotFoundError(keyring, args.mon) + + # mon. + keyring = '/var/lib/ceph/mon/{cluster}-{{hostname}}/keyring'.format( + cluster=args.cluster) + r = fetch_file( + args=args, + frompath=keyring, + topath='{cluster}.mon.keyring'.format(cluster=args.cluster), + _hosts=args.mon, + ) + if not r: + raise exc.KeyNotFoundError(keyring, args.mon) + + # bootstrap + for what in ['osd', 'mds', 'rgw']: + keyring = '/var/lib/ceph/bootstrap-{what}/{cluster}.keyring'.format( + what=what, + cluster=args.cluster) + r = fetch_file( + args=args, + frompath=keyring, + topath='{cluster}.bootstrap-{what}.keyring'.format( + cluster=args.cluster, + what=what), + _hosts=args.mon, + ) + if not r: + if what in ['osd', 'mds']: + raise exc.KeyNotFoundError(keyring, args.mon) + else: + LOG.warning(("No RGW bootstrap key found. Will not be able to " + "deploy RGW daemons")) + 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..7699a1c --- /dev/null +++ b/ceph_deploy/hosts/__init__.py @@ -0,0 +1,140 @@ +""" +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.hosts import debian, centos, fedora, suse, remotes, rhel +from ceph_deploy.connection import get_connection + +logger = logging.getLogger() + + +def get(hostname, + username=None, + fallback=None, + detect_sudo=True, + use_rhceph=False): + """ + 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. + """ + 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'] + module.is_rpm = module.normalized_name in ['redhat', 'centos', + 'fedora', 'scientific', 'suse'] + module.is_deb = not module.is_rpm + module.release = release + module.codename = codename + module.conn = conn + module.machine_type = machine_type + module.init = module.choose_init() + 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, + 'redhat': centos, + 'fedora': fedora, + 'suse': suse, + } + + 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(('suse', 'opensuse')): + return 'suse' + elif distro.startswith('centos'): + return 'centos' + elif distro.startswith('linuxmint'): + return 'ubuntu' + 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 + """ + release = release.strip() + + class NormalizedVersion(object): + pass + v = NormalizedVersion() # fake object to get nice dotted access + v.major, v.minor, v.patch, v.garbage = (release.split('.') + ["0"]*4)[:4] + release_map = dict(major=v.major, minor=v.minor, patch=v.patch, garbage=v.garbage) + + # safe int versions that remove non-numerical chars + # for example 'rc1' in a version like '1-rc1 + for name, value in release_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(v, int_name, value) + + return v diff --git a/ceph_deploy/hosts/centos/__init__.py b/ceph_deploy/hosts/centos/__init__.py new file mode 100644 index 0000000..9ac4683 --- /dev/null +++ b/ceph_deploy/hosts/centos/__init__.py @@ -0,0 +1,19 @@ +import mon # noqa +import pkg # noqa +from install import install, mirror_install, repo_install, repository_url_part, rpm_dist # noqa +from uninstall import uninstall # noqa + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + +def choose_init(): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + return 'sysvinit' diff --git a/ceph_deploy/hosts/centos/install.py b/ceph_deploy/hosts/centos/install.py new file mode 100644 index 0000000..fbec63f --- /dev/null +++ b/ceph_deploy/hosts/centos/install.py @@ -0,0 +1,226 @@ +from ceph_deploy.util import pkg_managers, templates +from ceph_deploy.lib import remoto +from ceph_deploy.hosts.util import install_yum_priorities +from ceph_deploy.util.paths import gpg + + +def rpm_dist(distro): + if distro.normalized_name in ['redhat', 'centos', 'scientific'] 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']: + return 'el' + distro.normalized_release.major + + return 'el6' + + +def install(distro, version_kind, version, adjust_repos, **kw): + # note: when split packages for ceph land for CentOS, `kw['components']` + # will have those. Unused for now. + logger = distro.conn.logger + release = distro.release + machine = distro.machine_type + repo_part = repository_url_part(distro) + dist = rpm_dist(distro) + + pkg_managers.yum_clean(distro.conn) + + # Get EPEL installed before we continue: + if adjust_repos: + install_epel(distro) + install_yum_priorities(distro) + 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 != 'dev': + remoto.process.run( + distro.conn, + [ + 'rpm', + '--import', + gpg.url(key) + ] + ) + + if version_kind == 'stable': + url = 'http://ceph.com/rpm-{version}/{repo}/'.format( + version=version, + repo=repo_part, + ) + elif version_kind == 'testing': + url = 'http://ceph.com/rpm-testing/{repo}/'.format(repo=repo_part) + + remoto.process.run( + distro.conn, + [ + 'rpm', + '-Uvh', + '--replacepkgs', + '{url}noarch/ceph-release-1-0.{dist}.noarch.rpm'.format(url=url, dist=dist), + ], + ) + + if version_kind == 'dev': + 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-centos{release}-{machine}-basic/ref/{version}/'.format( + release=release.split(".", 1)[0], + machine=machine, + version=version), + gpg.url(key), + adjust_repos=True, + extra_installs=False + ) + + # 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') + + remoto.process.run( + distro.conn, + [ + 'yum', + '-y', + 'install', + 'ceph', + 'ceph-radosgw', + ], + ) + + +def install_epel(distro): + """ + CentOS and Scientific need the EPEL repo, otherwise Ceph cannot be + installed. + """ + if distro.normalized_name in ['centos', 'scientific']: + distro.conn.logger.info('adding EPEL repository') + pkg_managers.yum(distro.conn, 'epel-release') + + +def mirror_install(distro, repo_url, gpg_url, adjust_repos, extra_installs=True, **kw): + # note: when split packages for ceph land for CentOS, `kw['components']` + # will have those. Unused for now. + repo_url = repo_url.strip('/') # Remove trailing slashes + gpg_url_path = gpg_url.split('file://')[-1] # Remove file if present + + pkg_managers.yum_clean(distro.conn) + + if adjust_repos: + remoto.process.run( + distro.conn, + [ + 'rpm', + '--import', + gpg_url_path, + ] + ) + + ceph_repo_content = templates.ceph_repo.format( + repo_url=repo_url, + gpg_url=gpg_url + ) + + distro.conn.remote_module.write_yum_repo(ceph_repo_content) + # set the right priority + install_yum_priorities(distro) + distro.conn.remote_module.set_repo_priority(['Ceph', 'Ceph-noarch', 'ceph-source']) + distro.conn.logger.warning('alter.d ceph.repo priorities to contain: priority=1') + + + if extra_installs: + pkg_managers.yum(distro.conn, 'ceph') + + +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 + # note: when split packages for ceph land for CentOS, `packages` + # can be used. Unused for now. + packages = kw.pop('components', []) # noqa + 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 + + pkg_managers.yum_clean(distro.conn) + + 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, + **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'): + install_yum_priorities(distro) + logger.warning( + 'ensuring that {repo_path} contains a high priority'.format( + repo_path=repo_path) + ) + + 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: + pkg_managers.yum(distro.conn, 'ceph') diff --git a/ceph_deploy/hosts/centos/mon/__init__.py b/ceph_deploy/hosts/centos/mon/__init__.py new file mode 100644 index 0000000..936d5d8 --- /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 create import create # noqa diff --git a/ceph_deploy/hosts/centos/mon/create.py b/ceph_deploy/hosts/centos/mon/create.py new file mode 100644 index 0000000..bfc6231 --- /dev/null +++ b/ceph_deploy/hosts/centos/mon/create.py @@ -0,0 +1,24 @@ +from ceph_deploy.hosts import common +from ceph_deploy.util import system +from ceph_deploy.lib import remoto + + +def create(distro, args, monitor_keyring): + hostname = distro.conn.remote_module.shortname() + common.mon_create(distro, args, monitor_keyring, hostname) + service = distro.conn.remote_module.which_service() + + remoto.process.run( + distro.conn, + [ + service, + 'ceph', + '-c', + '/etc/ceph/{cluster}.conf'.format(cluster=args.cluster), + 'start', + 'mon.{hostname}'.format(hostname=hostname) + ], + timeout=7, + ) + + system.enable_service(distro.conn) diff --git a/ceph_deploy/hosts/centos/pkg.py b/ceph_deploy/hosts/centos/pkg.py new file mode 100644 index 0000000..eb02bfd --- /dev/null +++ b/ceph_deploy/hosts/centos/pkg.py @@ -0,0 +1,15 @@ +from ceph_deploy.util import pkg_managers + + +def install(distro, packages): + return pkg_managers.yum( + distro.conn, + packages + ) + + +def remove(distro, packages): + return pkg_managers.yum_remove( + distro.conn, + packages + ) diff --git a/ceph_deploy/hosts/centos/uninstall.py b/ceph_deploy/hosts/centos/uninstall.py new file mode 100644 index 0000000..463892b --- /dev/null +++ b/ceph_deploy/hosts/centos/uninstall.py @@ -0,0 +1,17 @@ +from ceph_deploy.util import pkg_managers + + +def uninstall(conn, purge=False): + packages = [ + 'ceph', + 'ceph-release', + 'ceph-common', + 'ceph-radosgw', + ] + + pkg_managers.yum_remove( + conn, + packages, + ) + + pkg_managers.yum_clean(conn) diff --git a/ceph_deploy/hosts/common.py b/ceph_deploy/hosts/common.py new file mode 100644 index 0000000..b71db9d --- /dev/null +++ b/ceph_deploy/hosts/common.py @@ -0,0 +1,163 @@ +from ceph_deploy.util import paths +from ceph_deploy import conf +from ceph_deploy.lib import remoto +from StringIO import StringIO + + +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): + logger = distro.conn.logger + logger.debug('remote hostname: %s' % hostname) + path = paths.mon.path(args.cluster, hostname) + done_path = paths.mon.done(args.cluster, hostname) + init_path = paths.mon.init(args.cluster, hostname, distro.init) + + configuration = conf.ceph.load(args) + conf_data = StringIO() + configuration.write(conf_data) + + # write the configuration file + distro.conn.remote_module.write_conf( + args.cluster, + conf_data.getvalue(), + args.overwrite_conf, + ) + + # if the mon path does not exist, create it + distro.conn.remote_module.create_mon_path(path) + + 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, + ) + + remoto.process.run( + distro.conn, + [ + 'ceph-mon', + '--cluster', args.cluster, + '--mkfs', + '-i', hostname, + '--keyring', keyring, + ], + ) + + 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) + + # create init path + distro.conn.remote_module.create_init_path(init_path) + + +def mon_add(distro, args, monitor_keyring): + hostname = distro.conn.remote_module.shortname() + logger = distro.conn.logger + path = paths.mon.path(args.cluster, hostname) + 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) + + configuration = conf.ceph.load(args) + conf_data = StringIO() + configuration.write(conf_data) + + # write the configuration file + distro.conn.remote_module.write_conf( + args.cluster, + conf_data.getvalue(), + args.overwrite_conf, + ) + + # if the mon path does not exist, create it + distro.conn.remote_module.create_mon_path(path) + + 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, + ) + + # get the monmap + remoto.process.run( + distro.conn, + [ + 'ceph', + 'mon', + 'getmap', + '-o', + monmap_path, + ], + ) + + # now use it to prepare the monitor's data dir + remoto.process.run( + distro.conn, + [ + 'ceph-mon', + '--cluster', args.cluster, + '--mkfs', + '-i', hostname, + '--monmap', + monmap_path, + '--keyring', keyring, + ], + ) + + # add it + remoto.process.run( + distro.conn, + [ + 'ceph', + 'mon', + 'add', + hostname, + args.address, + ], + ) + + 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) + + # create init path + distro.conn.remote_module.create_init_path(init_path) + + # start the mon using the address + remoto.process.run( + distro.conn, + [ + 'ceph-mon', + '-i', + hostname, + '--public-addr', + args.address, + ], + ) diff --git a/ceph_deploy/hosts/debian/__init__.py b/ceph_deploy/hosts/debian/__init__.py new file mode 100644 index 0000000..654d838 --- /dev/null +++ b/ceph_deploy/hosts/debian/__init__.py @@ -0,0 +1,21 @@ +import mon # noqa +import pkg # noqa +from install import install, mirror_install, repo_install # noqa +from uninstall import uninstall # noqa + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + +def choose_init(): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + if distro.lower() == 'ubuntu': + return 'upstart' + return 'sysvinit' diff --git a/ceph_deploy/hosts/debian/install.py b/ceph_deploy/hosts/debian/install.py new file mode 100644 index 0000000..a16b394 --- /dev/null +++ b/ceph_deploy/hosts/debian/install.py @@ -0,0 +1,217 @@ +from urlparse import urlparse + +from ceph_deploy.lib import remoto +from ceph_deploy.util import pkg_managers +from ceph_deploy.util.paths import gpg + + +def install(distro, version_kind, version, adjust_repos, **kw): + # note: when split packages for ceph land for Debian/Ubuntu, + # `kw['components']` will have those. Unused for now. + codename = distro.codename + machine = distro.machine_type + + if version_kind in ['stable', 'testing']: + key = 'release' + else: + key = 'autobuild' + + # Make sure ca-certificates is installed + remoto.process.run( + distro.conn, + [ + 'env', + 'DEBIAN_FRONTEND=noninteractive', + 'apt-get', + '-q', + 'install', + '--assume-yes', + 'ca-certificates', + ] + ) + + if adjust_repos: + # Wheezy does not like the git.ceph.com SSL cert + protocol = 'https' + if codename == 'wheezy': + protocol = 'http' + remoto.process.run( + distro.conn, + [ + 'wget', + '-O', + '{key}.asc'.format(key=key), + gpg.url(key, protocol=protocol), + ], + stop_on_nonzero=False, + ) + + remoto.process.run( + distro.conn, + [ + 'apt-key', + 'add', + '{key}.asc'.format(key=key) + ] + ) + + if version_kind == 'stable': + url = 'http://ceph.com/debian-{version}/'.format( + version=version, + ) + elif version_kind == 'testing': + url = 'http://ceph.com/debian-testing/' + elif version_kind == 'dev': + url = 'http://gitbuilder.ceph.com/ceph-deb-{codename}-{machine}-basic/ref/{version}'.format( + codename=codename, + machine=machine, + version=version, + ) + 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) + + remoto.process.run( + distro.conn, + ['apt-get', '-q', 'update'], + ) + + # TODO this does not downgrade -- should it? + remoto.process.run( + distro.conn, + [ + 'env', + 'DEBIAN_FRONTEND=noninteractive', + 'DEBIAN_PRIORITY=critical', + 'apt-get', + '-q', + '-o', 'Dpkg::Options::=--force-confnew', + '--no-install-recommends', + '--assume-yes', + 'install', + '--', + 'ceph', + 'ceph-mds', + 'ceph-common', + 'ceph-fs-common', + 'radosgw', + # ceph only recommends gdisk, make sure we actually have + # it; only really needed for osds, but minimal collateral + 'gdisk', + ], + ) + + +def mirror_install(distro, repo_url, gpg_url, adjust_repos, **kw): + # note: when split packages for ceph land for Debian/Ubuntu, + # `kw['components']` will have those. Unused for now. + repo_url = repo_url.strip('/') # Remove trailing slashes + gpg_path = gpg_url.split('file://')[-1] + + if adjust_repos: + if not gpg_url.startswith('file://'): + remoto.process.run( + distro.conn, + [ + 'wget', + '-O', + 'release.asc', + gpg_url, + ], + stop_on_nonzero=False, + ) + + gpg_file = 'release.asc' if not gpg_url.startswith('file://') else gpg_path + remoto.process.run( + distro.conn, + [ + 'apt-key', + 'add', + gpg_file, + ] + ) + + # 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) + + pkg_managers.apt_update(distro.conn) + packages = ( + 'ceph', + 'ceph-mds', + 'ceph-common', + 'ceph-fs-common', + # ceph only recommends gdisk, make sure we actually have + # it; only really needed for osds, but minimal collateral + 'gdisk', + ) + + pkg_managers.apt(distro.conn, packages) + pkg_managers.apt(distro.conn, 'ceph') + + +def repo_install(distro, repo_name, baseurl, gpgkey, **kw): + # do we have specific components to install? + # removed them from `kw` so that we don't mess with other defaults + # note: when split packages for ceph land for Debian/Ubuntu, `packages` + # can be used. Unused for now. + 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 + + if gpgkey: + remoto.process.run( + distro.conn, + [ + 'wget', + '-O', + 'release.asc', + gpgkey, + ], + stop_on_nonzero=False, + ) + + remoto.process.run( + distro.conn, + [ + 'apt-key', + 'add', + 'release.asc' + ] + ) + + 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 + pkg_managers.apt_update(distro.conn) + + if install_ceph: + # Before any install, make sure we have `wget` + packages = ( + 'ceph', + 'ceph-mds', + 'ceph-common', + 'ceph-fs-common', + # ceph only recommends gdisk, make sure we actually have + # it; only really needed for osds, but minimal collateral + 'gdisk', + ) + + pkg_managers.apt(distro.conn, packages) + pkg_managers.apt(distro.conn, 'ceph') diff --git a/ceph_deploy/hosts/debian/mon/__init__.py b/ceph_deploy/hosts/debian/mon/__init__.py new file mode 100644 index 0000000..936d5d8 --- /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 create import create # noqa diff --git a/ceph_deploy/hosts/debian/mon/create.py b/ceph_deploy/hosts/debian/mon/create.py new file mode 100644 index 0000000..93d4393 --- /dev/null +++ b/ceph_deploy/hosts/debian/mon/create.py @@ -0,0 +1,42 @@ +from ceph_deploy.hosts import common +from ceph_deploy.lib import remoto + + +def create(distro, args, monitor_keyring): + logger = distro.conn.logger + hostname = distro.conn.remote_module.shortname() + common.mon_create(distro, args, monitor_keyring, hostname) + service = distro.conn.remote_module.which_service() + + if not service: + logger.warning('could not find `service` executable') + + if distro.init == 'upstart': # Ubuntu uses upstart + remoto.process.run( + distro.conn, + [ + 'initctl', + 'emit', + 'ceph-mon', + 'cluster={cluster}'.format(cluster=args.cluster), + 'id={hostname}'.format(hostname=hostname), + ], + timeout=7, + ) + + elif distro.init == 'sysvinit': # Debian uses sysvinit + + remoto.process.run( + distro.conn, + [ + service, + 'ceph', + '-c', + '/etc/ceph/{cluster}.conf'.format(cluster=args.cluster), + 'start', + 'mon.{hostname}'.format(hostname=hostname) + ], + timeout=7, + ) + else: + raise RuntimeError('create cannot use init %s' % distro.init) diff --git a/ceph_deploy/hosts/debian/pkg.py b/ceph_deploy/hosts/debian/pkg.py new file mode 100644 index 0000000..40a94da --- /dev/null +++ b/ceph_deploy/hosts/debian/pkg.py @@ -0,0 +1,15 @@ +from ceph_deploy.util import pkg_managers + + +def install(distro, packages): + return pkg_managers.apt( + distro.conn, + packages + ) + + +def remove(distro, packages): + return pkg_managers.apt_remove( + distro.conn, + packages + ) diff --git a/ceph_deploy/hosts/debian/uninstall.py b/ceph_deploy/hosts/debian/uninstall.py new file mode 100644 index 0000000..ce2013f --- /dev/null +++ b/ceph_deploy/hosts/debian/uninstall.py @@ -0,0 +1,16 @@ +from ceph_deploy.util import pkg_managers + + +def uninstall(conn, purge=False): + packages = [ + 'ceph', + 'ceph-mds', + 'ceph-common', + 'ceph-fs-common', + 'radosgw', + ] + pkg_managers.apt_remove( + conn, + packages, + purge=purge, + ) diff --git a/ceph_deploy/hosts/fedora/__init__.py b/ceph_deploy/hosts/fedora/__init__.py new file mode 100644 index 0000000..bdb8405 --- /dev/null +++ b/ceph_deploy/hosts/fedora/__init__.py @@ -0,0 +1,20 @@ +import mon # noqa +from ceph_deploy.hosts.centos import pkg # noqa +from ceph_deploy.hosts.centos.install import repo_install # noqa +from install import install, mirror_install # noqa +from uninstall import uninstall # noqa + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + +def choose_init(): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + return 'sysvinit' diff --git a/ceph_deploy/hosts/fedora/install.py b/ceph_deploy/hosts/fedora/install.py new file mode 100644 index 0000000..900fbab --- /dev/null +++ b/ceph_deploy/hosts/fedora/install.py @@ -0,0 +1,88 @@ +from ceph_deploy.lib import remoto +from ceph_deploy.hosts.centos.install import repo_install, mirror_install # noqa +from ceph_deploy.hosts.util import install_yum_priorities +from ceph_deploy.util.paths import gpg + + +def install(distro, version_kind, version, adjust_repos, **kw): + # note: when split packages for ceph land for Fedora, + # `kw['components']` will have those. Unused for now. + 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: + install_yum_priorities(distro) + distro.conn.remote_module.enable_yum_priority_obsoletes() + logger.warning('check_obsoletes has been enabled for Yum priorities plugin') + + if version_kind != 'dev': + remoto.process.run( + distro.conn, + [ + 'rpm', + '--import', + gpg.url(key) + ] + ) + + if version_kind == 'stable': + url = 'http://ceph.com/rpm-{version}/fc{release}/'.format( + version=version, + release=release, + ) + elif version_kind == 'testing': + url = 'http://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, + ), + ] + ) + + if version_kind == 'dev': + 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/ref/{version}/'.format( + release=release.split(".", 1)[0], + machine=machine, + version=version), + gpg.url(key), + adjust_repos=True, + extra_installs=False + ) + + # 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') + + remoto.process.run( + distro.conn, + [ + 'yum', + '-y', + '-q', + 'install', + 'ceph', + 'ceph-radosgw', + ], + ) diff --git a/ceph_deploy/hosts/fedora/mon/__init__.py b/ceph_deploy/hosts/fedora/mon/__init__.py new file mode 100644 index 0000000..936d5d8 --- /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 create import create # noqa diff --git a/ceph_deploy/hosts/fedora/mon/create.py b/ceph_deploy/hosts/fedora/mon/create.py new file mode 100644 index 0000000..d0dddbd --- /dev/null +++ b/ceph_deploy/hosts/fedora/mon/create.py @@ -0,0 +1,21 @@ +from ceph_deploy.hosts import common +from ceph_deploy.lib import remoto + + +def create(distro, args, monitor_keyring): + hostname = distro.conn.remote_module.shortname() + common.mon_create(distro, args, monitor_keyring, hostname) + service = distro.conn.remote_module.which_service() + + remoto.process.run( + distro.conn, + [ + service, + 'ceph', + '-c', + '/etc/ceph/{cluster}.conf'.format(cluster=args.cluster), + 'start', + 'mon.{hostname}'.format(hostname=hostname) + ], + timeout=7, + ) diff --git a/ceph_deploy/hosts/fedora/uninstall.py b/ceph_deploy/hosts/fedora/uninstall.py new file mode 100644 index 0000000..1b032c8 --- /dev/null +++ b/ceph_deploy/hosts/fedora/uninstall.py @@ -0,0 +1,15 @@ +from ceph_deploy.util import pkg_managers + + +def uninstall(conn, purge=False): + packages = [ + 'ceph', + 'ceph-common', + 'ceph-radosgw', + ] + + pkg_managers.yum_remove( + conn, + packages, + ) + diff --git a/ceph_deploy/hosts/remotes.py b/ceph_deploy/hosts/remotes.py new file mode 100644 index 0000000..ba3e7d8 --- /dev/null +++ b/ceph_deploy/hosts/remotes.py @@ -0,0 +1,343 @@ +import ConfigParser +import errno +import socket +import os +import shutil +import tempfile +import platform + + +def platform_information(_linux_distribution=None): + """ detect platform information from remote host """ + linux_distribution = _linux_distribution or platform.linux_distribution + distro, release, codename = linux_distribution() + if not codename and 'debian' in distro.lower(): # this could be an empty string in Debian + debian_codenames = { + '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 + + return ( + str(distro).rstrip(), + str(release).rstrip(), + str(codename).rstrip() + ) + + +def machine_type(): + """ detect machine type """ + return platform.machine() + + +def write_sources_list(url, codename, filename='ceph.list'): + """add deb repo to sources.list""" + repo_path = os.path.join('/etc/apt/sources.list.d', filename) + with file(repo_path, 'w') as f: + f.write('deb {url} {codename} main\n'.format( + url=url, + codename=codename, + )) + + +def write_yum_repo(content, filename='ceph.repo'): + """set the contents of repo file to /etc/yum.repos.d/""" + repo_path = os.path.join('/etc/yum.repos.d', filename) + write_file(repo_path, content) + + +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, 'wb') 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, 'wb') 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 = file(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 = 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(dir='/etc/ceph', delete=False) + err_msg = 'config file %s exists with different content; use --overwrite-conf to overwrite' % path + + if os.path.exists(path): + with file(path, 'rb') 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) + os.chmod(path, 0644) + return + if os.path.exists('/etc/ceph'): + with open(path, 'w') as f: + f.write(conf) + os.chmod(path, 0644) + else: + err_msg = '/etc/ceph/ does not exist - could not write config' + raise RuntimeError(err_msg) + + +def write_keyring(path, key): + """ 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(delete=False) + tmp_file.write(key) + tmp_file.close() + keyring_dir = os.path.dirname(path) + if not path_exists(keyring_dir): + makedir(keyring_dir) + shutil.move(tmp_file.name, path) + + +def create_mon_path(path): + """create the mon path if it does not exist""" + if not os.path.exists(path): + os.makedirs(path) + + +def create_done_path(done_path): + """create a done file to avoid re-doing the mon deployment""" + with file(done_path, 'w'): + pass + + +def create_init_path(init_path): + """create the init path if it does not exist""" + if not os.path.exists(init_path): + with file(init_path, 'w'): + pass + + +def append_to_file(file_path, contents): + """append contents to file""" + with open(file_path, 'a') as f: + f.write(contents) + + +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): + ignored = ignored or [] + try: + os.makedirs(path) + except OSError as error: + if error.errno in ignored: + pass + else: + # re-raise the original exception + raise + + +def unlink(_file): + os.unlink(_file) + + +def write_monitor_keyring(keyring, monitor_keyring): + """create the monitor keyring file""" + write_file(keyring, monitor_keyring) + + +def write_file(path, content, mode=0644, directory=None): + if directory: + if path.startswith("/"): + path = path[1:] + path = os.path.join(directory, path) + with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT, mode), 'w') as f: + f.write(content) + + +def touch_file(path): + with file(path, 'wb') as f: # noqa + pass + + +def get_file(path): + """ fetch remote file """ + try: + with file(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): + 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, e: + if e.errno != errno.EEXIST: + raise + shutil.move(path, os.path.join('/var/lib/ceph/mon-removed/', file_name)) + + +def safe_mkdir(path): + """ create path if it doesn't exist """ + try: + os.mkdir(path) + except OSError, e: + if e.errno == errno.EEXIST: + pass + else: + raise + + +def safe_makedirs(path): + """ create path recursively if it doesn't exist """ + try: + os.makedirs(path) + except OSError, e: + if e.errno == errno.EEXIST: + pass + else: + raise + + +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 file(dev, 'wb') as f: + f.seek(-size, os.SEEK_END) + f.write(size*'\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, 'wb') 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..13f7b22 --- /dev/null +++ b/ceph_deploy/hosts/rhel/__init__.py @@ -0,0 +1,19 @@ +import mon # noqa +import pkg # noqa +from install import install, mirror_install, repo_install # noqa +from uninstall import uninstall # noqa + +# Allow to set some information about this distro +# + +distro = None +release = None +codename = None + +def choose_init(): + """ + Select a init system + + Returns the name of a init system (upstart, sysvinit ...). + """ + return 'sysvinit' diff --git a/ceph_deploy/hosts/rhel/install.py b/ceph_deploy/hosts/rhel/install.py new file mode 100644 index 0000000..9a44cdd --- /dev/null +++ b/ceph_deploy/hosts/rhel/install.py @@ -0,0 +1,85 @@ +from ceph_deploy.util import pkg_managers, templates +from ceph_deploy.lib import remoto + + +def install(distro, version_kind, version, adjust_repos, **kw): + packages = kw.get('components', []) + pkg_managers.yum_clean(distro.conn) + pkg_managers.yum(distro.conn, 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 + gpg_url_path = gpg_url.split('file://')[-1] # Remove file if present + + pkg_managers.yum_clean(distro.conn) + + if adjust_repos: + remoto.process.run( + distro.conn, + [ + 'rpm', + '--import', + gpg_url_path, + ] + ) + + ceph_repo_content = templates.ceph_repo.format( + repo_url=repo_url, + gpg_url=gpg_url + ) + + distro.conn.remote_module.write_yum_repo(ceph_repo_content) + + if extra_installs: + pkg_managers.yum(distro.conn, 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 + + pkg_managers.yum_clean(distro.conn) + + 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, + **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: + pkg_managers.yum(distro.conn, 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..936d5d8 --- /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 create import create # noqa diff --git a/ceph_deploy/hosts/rhel/mon/create.py b/ceph_deploy/hosts/rhel/mon/create.py new file mode 100644 index 0000000..bfc6231 --- /dev/null +++ b/ceph_deploy/hosts/rhel/mon/create.py @@ -0,0 +1,24 @@ +from ceph_deploy.hosts import common +from ceph_deploy.util import system +from ceph_deploy.lib import remoto + + +def create(distro, args, monitor_keyring): + hostname = distro.conn.remote_module.shortname() + common.mon_create(distro, args, monitor_keyring, hostname) + service = distro.conn.remote_module.which_service() + + remoto.process.run( + distro.conn, + [ + service, + 'ceph', + '-c', + '/etc/ceph/{cluster}.conf'.format(cluster=args.cluster), + 'start', + 'mon.{hostname}'.format(hostname=hostname) + ], + timeout=7, + ) + + system.enable_service(distro.conn) diff --git a/ceph_deploy/hosts/rhel/pkg.py b/ceph_deploy/hosts/rhel/pkg.py new file mode 100644 index 0000000..eb02bfd --- /dev/null +++ b/ceph_deploy/hosts/rhel/pkg.py @@ -0,0 +1,15 @@ +from ceph_deploy.util import pkg_managers + + +def install(distro, packages): + return pkg_managers.yum( + distro.conn, + packages + ) + + +def remove(distro, packages): + return pkg_managers.yum_remove( + distro.conn, + packages + ) diff --git a/ceph_deploy/hosts/rhel/uninstall.py b/ceph_deploy/hosts/rhel/uninstall.py new file mode 100644 index 0000000..8660505 --- /dev/null +++ b/ceph_deploy/hosts/rhel/uninstall.py @@ -0,0 +1,18 @@ +from ceph_deploy.util import pkg_managers + + +def uninstall(conn, purge=False): + packages = [ + 'ceph', + 'ceph-common', + 'ceph-mon', + 'ceph-osd', + 'ceph-radosgw' + ] + + pkg_managers.yum_remove( + conn, + packages, + ) + + pkg_managers.yum_clean(conn) diff --git a/ceph_deploy/hosts/suse/__init__.py b/ceph_deploy/hosts/suse/__init__.py new file mode 100644 index 0000000..65b1255 --- /dev/null +++ b/ceph_deploy/hosts/suse/__init__.py @@ -0,0 +1,26 @@ +import mon # noqa +import pkg # noqa +from install import install, mirror_install, repo_install # noqa +from uninstall import uninstall # noqa +import logging + +# Allow to set some information about this distro +# + +log = logging.getLogger(__name__) + +distro = None +release = None +codename = None + +def choose_init(): + """ + 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, 'sysvinit') diff --git a/ceph_deploy/hosts/suse/install.py b/ceph_deploy/hosts/suse/install.py new file mode 100644 index 0000000..c574e55 --- /dev/null +++ b/ceph_deploy/hosts/suse/install.py @@ -0,0 +1,189 @@ +from ceph_deploy.util import templates, pkg_managers +from ceph_deploy.lib import remoto +from ceph_deploy.util.paths import gpg +import logging + +LOG = logging.getLogger(__name__) + + +def install(distro, version_kind, version, adjust_repos, **kw): + # note: when split packages for ceph land for Suse, + # `kw['components']` will have those. Unused for now. + release = distro.release + machine = distro.machine_type + + if version_kind in ['stable', 'testing']: + key = 'release' + else: + key = 'autobuild' + + + distro_name = None + if distro.codename == 'Mantis': + distro_name = 'opensuse12.2' + + if (distro.name == "SUSE Linux Enterprise Server") and (str(distro.release) == "11"): + distro_name = 'sles11' + + if distro_name == None: + LOG.warning('Untested version of %s: assuming compatible with SUSE Linux Enterprise Server 11', distro.name) + distro_name = 'sles11' + + + if adjust_repos: + # Work around code due to bug in SLE 11 + # https://bugzilla.novell.com/show_bug.cgi?id=875170 + protocol = "https" + if distro_name == 'sles11': + protocol = "http" + remoto.process.run( + distro.conn, + [ + 'rpm', + '--import', + gpg.url(key, protocol=protocol) + ] + ) + + if version_kind == 'stable': + url = 'http://ceph.com/rpm-{version}/{distro}/'.format( + version=version, + distro=distro_name, + ) + elif version_kind == 'testing': + url = 'http://ceph.com/rpm-testing/{distro}/'.format(distro=distro_name) + elif version_kind == 'dev': + url = 'http://gitbuilder.ceph.com/ceph-rpm-{distro}{release}-{machine}-basic/ref/{version}/'.format( + distro=distro_name, + release=release.split(".", 1)[0], + machine=machine, + version=version, + ) + + remoto.process.run( + distro.conn, + [ + 'rpm', + '-Uvh', + '--replacepkgs', + '--force', + '--quiet', + '{url}ceph-release-1-0.noarch.rpm'.format( + url=url, + ), + ] + ) + + remoto.process.run( + distro.conn, + [ + 'zypper', + '--non-interactive', + 'refresh' + ], + ) + + remoto.process.run( + distro.conn, + [ + 'zypper', + '--non-interactive', + '--quiet', + 'install', + 'ceph', + 'ceph-radosgw', + ], + ) + + +def mirror_install(distro, repo_url, gpg_url, adjust_repos, **kw): + # note: when split packages for ceph land for Suse, + # `kw['components']` will have those. Unused for now. + repo_url = repo_url.strip('/') # Remove trailing slashes + gpg_url_path = gpg_url.split('file://')[-1] # Remove file if present + + 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 + ) + distro.conn.remote_module.write_file( + '/etc/zypp/repos.d/ceph.repo', + ceph_repo_content) + remoto.process.run( + distro.conn, + [ + 'zypper', + '--non-interactive', + 'refresh' + ] + ) + + remoto.process.run( + distro.conn, + [ + 'zypper', + '--non-interactive', + '--quiet', + 'install', + 'ceph', + ], + ) + + +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 + # note: when split packages for ceph land for Suse, `packages` + # can be used. Unused for now. + packages = kw.pop('components', []) # noqa + # 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 + ) + + # Some custom repos do not need to install ceph + if install_ceph: + # Before any install, make sure we have `wget` + pkg_managers.zypper(distro.conn, 'wget') + + pkg_managers.zypper(distro.conn, 'ceph') diff --git a/ceph_deploy/hosts/suse/mon/__init__.py b/ceph_deploy/hosts/suse/mon/__init__.py new file mode 100644 index 0000000..936d5d8 --- /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 create import create # noqa diff --git a/ceph_deploy/hosts/suse/mon/create.py b/ceph_deploy/hosts/suse/mon/create.py new file mode 100644 index 0000000..5c0b9bf --- /dev/null +++ b/ceph_deploy/hosts/suse/mon/create.py @@ -0,0 +1,19 @@ +from ceph_deploy.hosts import common +from ceph_deploy.lib import remoto + + +def create(distro, args, monitor_keyring): + hostname = distro.conn.remote_module.shortname() + common.mon_create(distro, args, monitor_keyring, hostname) + + remoto.process.run( + distro.conn, + [ + 'rcceph', + '-c', + '/etc/ceph/{cluster}.conf'.format(cluster=args.cluster), + 'start', + 'mon.{hostname}'.format(hostname=hostname) + ], + timeout=7, + ) diff --git a/ceph_deploy/hosts/suse/pkg.py b/ceph_deploy/hosts/suse/pkg.py new file mode 100644 index 0000000..da43279 --- /dev/null +++ b/ceph_deploy/hosts/suse/pkg.py @@ -0,0 +1,15 @@ +from ceph_deploy.util import pkg_managers + + +def install(distro, packages): + return pkg_managers.zypper( + distro.conn, + packages + ) + + +def remove(distro, packages): + return pkg_managers.zypper_remove( + distro.conn, + packages + ) diff --git a/ceph_deploy/hosts/suse/uninstall.py b/ceph_deploy/hosts/suse/uninstall.py new file mode 100644 index 0000000..b67a7a2 --- /dev/null +++ b/ceph_deploy/hosts/suse/uninstall.py @@ -0,0 +1,20 @@ +from ceph_deploy.lib import remoto + + +def uninstall(conn, purge=False): + packages = [ + 'ceph', + 'libcephfs1', + 'librados2', + 'librbd1', + 'ceph-radosgw', + ] + cmd = [ + 'zypper', + '--non-interactive', + '--quiet', + 'remove', + ] + + cmd.extend(packages) + remoto.process.run(conn, cmd) 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..49c8be2 --- /dev/null +++ b/ceph_deploy/install.py @@ -0,0 +1,633 @@ +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 = 'hammer' + 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-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_common': 'ceph-common', + } + + if distro.is_rpm: + defaults = default_components.rpm + 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) + + 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: + 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, + ) + + # 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, + ) + + # 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 uninstall(args): + LOG.info('note that some dependencies *will not* be removed because they can cause issues with qemu-kvm') + LOG.info('like: librbd1 and librados2') + LOG.debug( + 'Uninstalling on cluster %s hosts %s', + 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('uninstalling ceph on %s' % hostname) + distro.uninstall(distro.conn) + distro.conn.exit() + + +def purge(args): + LOG.info('note that some dependencies *will not* be removed because they can cause issues with qemu-kvm') + LOG.info('like: librbd1 and librados2') + + LOG.debug( + 'Purging from cluster %s hosts %s', + 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('purging host ... %s' % hostname) + distro.uninstall(distro.conn, purge=True) + distro.conn.exit() + + +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)', + ) + + version.add_argument( + '--mon', + dest='install_mon', + action='store_true', + help='install the mon component only', + ) + + version.add_argument( + '--mds', + dest='install_mds', + action='store_true', + help='install the mds component only', + ) + + version.add_argument( + '--rgw', + dest='install_rgw', + action='store_true', + help='install the rgw component only', + ) + + version.add_argument( + '--osd', + dest='install_osd', + action='store_true', + help='install the osd component only', + ) + + version.add_argument( + '--cli', '--common', + dest='install_common', + action='store_true', + help='install the common component only', + ) + + version.add_argument( + '--all', + dest='install_all', + action='store_true', + help='install all ceph components (e.g. mon,osd,mds,rgw). This is the default', + ) + + version.add_argument( + '--adjust-repos', + dest='adjust_repos', + action='store_true', + help='install packages modifying source repos', + ) + + version.add_argument( + '--no-adjust-repos', + dest='adjust_repos', + action='store_false', + help='install packages without modifying source repos', + ) + + version.set_defaults( + func=install, + stable=None, # XXX deprecated in favor of release + release=None, # Set the default release in sanitize_args() + dev='master', + version_kind='stable', + adjust_repos=True, + ) + + parser.add_argument( + '--repo', + action='store_true', + help='install repo files only (skips package installation)', + ) + + 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.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..f48fd74 --- /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..4304419 --- /dev/null +++ b/ceph_deploy/mds.py @@ -0,0 +1,229 @@ +from cStringIO import StringIO +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_mds_key(cluster): + """ + Read the bootstrap-mds key for `cluster`. + """ + path = '{cluster}.bootstrap-mds.keyring'.format(cluster=cluster) + try: + with file(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 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 mds') + + 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 *', + 'mds', 'allow', + 'mon', 'allow rwx', + '-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', + '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 + ) + elif init == 'systemd': + remoto.process.run( + conn, + [ + 'systemctl', + 'enable', + 'ceph-mds@{name}'.format(name=name), + ], + timeout=7 + ) + + if distro.is_el: + system.enable_service(distro.conn) + + +def mds_create(args): + cfg = conf.ceph.load(args) + LOG.debug( + 'Deploying mds, cluster %s hosts %s', + args.cluster, + ' '.join(':'.join(x or '' for x in t) for t in args.mds), + ) + + if not args.mds: + raise exc.NeedHostError() + + key = get_bootstrap_mds_key(cluster=args.cluster) + + bootstrapped = set() + errors = 0 + failed_on_rhel = False + + for hostname, name in args.mds: + 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 mds bootstrap to %s', hostname) + conf_data = StringIO() + cfg.write(conf_data) + distro.conn.remote_module.write_conf( + args.cluster, + conf_data.getvalue(), + 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.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_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/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..b0e3e8e --- /dev/null +++ b/ceph_deploy/mon.py @@ -0,0 +1,598 @@ +import argparse +import json +import logging +import re +import os +from textwrap import dedent +import time + +from ceph_deploy import conf, exc, admin +from ceph_deploy.cliutil import priority +from ceph_deploy.util import paths, net, files +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.connection import get_connection +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) + + if not args.mon: + raise exc.NeedHostError() + elif len(args.mon) > 1: + raise exc.GenericError('Only one node can be added at a time') + mon_host = args.mon[0] + + try: + with file('{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 = [mon_host] + 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) + 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) + 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')): + 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), + ] + + 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) + 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 + 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) + rconn = get_connection(host, username=args.username, logger=rlogger) + while tries: + status = mon_status_check(rconn, 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 + rconn.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): + """ + Deploy ceph monitor on remote hosts. + """ + sub_command_help = dedent(""" + Subcommands: + + 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. + + create + Deploy monitors by specifying them 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. + + add + Add a monitor to an existing cluster: + + ceph-deploy mon add node1 + + Or: + + ceph-deploy mon add node1 --address 192.168.1.10 + + If the section for the monitor exists and defines a `mon addr` that + will be used, otherwise it will fallback by resolving the hostname to an + IP. If `--address` is used it will override all other options. Please + note that only one node can be added at a time. + + destroy + Completely remove monitors on a remote host. Requires hostname(s) as + arguments. + """) + parser.formatter_class = argparse.RawDescriptionHelpFormatter + parser.description = sub_command_help + + mon_parser = parser.add_subparsers(dest='subcommand') + + mon_add = mon_parser.add_parser( + 'add', + help='Add a new Ceph MON to an existing cluster' + ) + mon_add.add_argument( + '--address', + nargs='?', + ) + mon_add.add_argument( + 'mon', + nargs='*', + ) + + mon_create = mon_parser.add_parser( + 'create', + help='Deploy new Ceph MON(s) as part of creating a new cluster' + ) + 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='Deploy new Ceph MON(s) from ceph.conf and ensure quorum' + ) + 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', ' 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..197f992 --- /dev/null +++ b/ceph_deploy/new.py @@ -0,0 +1,279 @@ +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 prepare_disk( + conn, + cluster, + disk, + journal, + activate_prepared_disk, + zap, + fs_type, + dmcrypt, + dmcrypt_dir): + """ + Run on osd node, prepares a data disk for use. + """ + args = [ + 'ceph-disk', + '-v', + 'prepare', + ] + if zap: + args.append('--zap-disk') + if fs_type: + if fs_type not in ('btrfs', 'ext4', 'xfs'): + raise argparse.ArgumentTypeError( + "FS_TYPE must be one of 'btrfs', 'ext4' or 'xfs'") + args.extend(['--fs-type', fs_type]) + if dmcrypt: + args.append('--dmcrypt') + if dmcrypt_dir is not None: + args.append('--dmcrypt-key-dir') + args.append(dmcrypt_dir) + args.extend([ + '--cluster', + cluster, + '--', + disk, + ]) + + if journal is not None: + args.append(journal) + + remoto.process.run( + conn, + args + ) + + if activate_prepared_disk: + return remoto.process.run( + conn, + [ + 'udevadm', + 'trigger', + '--subsystem-match=block', + '--action=add', + ], + ) + + +def exceeds_max_osds(args, reasonable=20): + """ + A very simple function to check against multiple OSDs getting created and + warn about the possibility of more than the recommended which would cause + issues with max allowed PIDs in a system. + + The check is done against the ``args.disk`` object that should look like:: + + [ + ('cephnode-01', '/dev/sdb', '/dev/sda5'), + ('cephnode-01', '/dev/sdc', '/dev/sda6'), + ... + ] + """ + hosts = [item[0] for item in args.disk] + per_host_count = dict( + ( + (h, hosts.count(h)) for h in set(hosts) + if hosts.count(h) > reasonable + ) + ) + + return per_host_count + + +def prepare(args, cfg, activate_prepared_disk): + LOG.debug( + 'Preparing cluster %s disks %s', + args.cluster, + ' '.join(':'.join(x or '' for x in t) for t in args.disk), + ) + + hosts_in_danger = exceeds_max_osds(args) + + if hosts_in_danger: + LOG.warning('if ``kernel.pid_max`` is not increased to a high enough value') + LOG.warning('the following hosts will encounter issues:') + for host, count in hosts_in_danger.items(): + LOG.warning('Host: %8s, OSDs: %s' % (host, count)) + + key = get_bootstrap_osd_key(cluster=args.cluster) + + bootstrapped = set() + errors = 0 + for hostname, disk, journal in args.disk: + try: + if disk is None: + raise exc.NeedDiskError(hostname) + + distro = hosts.get(hostname, username=args.username) + 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 = StringIO() + cfg.write(conf_data) + distro.conn.remote_module.write_conf( + args.cluster, + conf_data.getvalue(), + args.overwrite_conf + ) + + create_osd(distro.conn, args.cluster, key) + + LOG.debug('Preparing host %s disk %s journal %s activate %s', + hostname, disk, journal, activate_prepared_disk) + + prepare_disk( + distro.conn, + cluster=args.cluster, + disk=disk, + journal=journal, + activate_prepared_disk=activate_prepared_disk, + zap=args.zap_disk, + fs_type=args.fs_type, + dmcrypt=args.dmcrypt, + dmcrypt_dir=args.dmcrypt_key_dir, + ) + + # 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 activate(args, cfg): + LOG.debug( + 'Activating cluster %s disks %s', + args.cluster, + # join elements of t with ':', t's with ' ' + # allow None in elements of t; print as empty + ' '.join(':'.join((s or '') for s in t) for t in args.disk), + ) + + for hostname, disk, journal in args.disk: + + distro = hosts.get(hostname, username=args.username) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + LOG.debug('activating host %s disk %s', hostname, disk) + LOG.debug('will use init type: %s', distro.init) + + remoto.process.run( + distro.conn, + [ + 'ceph-disk', + '-v', + 'activate', + '--mark-init', + distro.init, + '--mount', + disk, + ], + ) + # give the OSD a few seconds to start + time.sleep(5) + catch_osd_errors(distro.conn, distro.conn.logger, args) + + if distro.is_el: + system.enable_service(distro.conn) + + distro.conn.exit() + + +def disk_zap(args): + + for hostname, disk, journal 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) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + distro.conn.remote_module.zeroing(disk) + + ceph_disk_executable = system.executable_path(distro.conn, 'ceph-disk') + remoto.process.run( + distro.conn, + [ + ceph_disk_executable, + 'zap', + disk, + ], + ) + + # once all is done, call partprobe (or partx) + # On RHEL and CentOS distros, calling partprobe forces a reboot of the + # server. Since we are not resizing partitons we rely on calling + # partx + if distro.normalized_name.startswith(('centos', 'red')): + LOG.info('calling partx on zapped device %s', disk) + LOG.info('re-reading known partitions will display errors') + remoto.process.run( + distro.conn, + [ + 'partx', + '-a', + disk, + ], + ) + + else: + LOG.debug('Calling partprobe on zapped device %s', disk) + remoto.process.run( + distro.conn, + [ + 'partprobe', + disk, + ], + ) + + distro.conn.exit() + + +def disk_list(args, cfg): + for hostname, disk, journal in args.disk: + distro = hosts.get(hostname, username=args.username) + LOG.info( + 'Distro info: %s %s %s', + distro.name, + distro.release, + distro.codename + ) + + LOG.debug('Listing disks on {hostname}...'.format(hostname=hostname)) + ceph_disk_executable = system.executable_path(distro.conn, 'ceph-disk') + remoto.process.run( + distro.conn, + [ + ceph_disk_executable, + 'list', + ], + ) + distro.conn.exit() + + +def osd_list(args, cfg): + monitors = mon.get_mon_initial_members(args, error_on_empty=True, _cfg=cfg) + + # get the osd tree from a monitor host + mon_host = monitors[0] + distro = hosts.get(mon_host, username=args.username) + tree = osd_tree(distro.conn, args.cluster) + distro.conn.exit() + + interesting_files = ['active', 'magic', 'whoami', 'journal_uuid'] + + for hostname, disk, journal in args.disk: + distro = hosts.get(hostname, username=args.username) + remote_module = distro.conn.remote_module + osds = distro.conn.remote_module.listdir(constants.osd_path) + + ceph_disk_executable = system.executable_path(distro.conn, 'ceph-disk') + output, err, exit_code = remoto.process.check( + distro.conn, + [ + ceph_disk_executable, + 'list', + ] + ) + + for _osd in osds: + osd_path = os.path.join(constants.osd_path, _osd) + journal_path = os.path.join(osd_path, 'journal') + _id = int(_osd.split('-')[-1]) # split on dash, get the id + osd_name = 'osd.%s' % _id + metadata = {} + json_blob = {} + + # piggy back from ceph-disk and get the mount point + device = get_osd_mount_point(output, osd_name) + if device: + metadata['device'] = device + + # read interesting metadata from files + for f in interesting_files: + osd_f_path = os.path.join(osd_path, f) + if remote_module.path_exists(osd_f_path): + metadata[f] = remote_module.readline(osd_f_path) + + # do we have a journal path? + if remote_module.path_exists(journal_path): + metadata['journal path'] = remote_module.get_realpath(journal_path) + + # is this OSD in osd tree? + for blob in tree['nodes']: + if blob.get('id') == _id: # matches our OSD + json_blob = blob + + print_osd( + distro.conn.logger, + hostname, + osd_path, + json_blob, + metadata, + ) + + distro.conn.exit() + + +def get_osd_mount_point(output, osd_name): + """ + piggy back from `ceph-disk list` output and get the mount point + by matching the line where the partition mentions the OSD name + + For example, if the name of the osd is `osd.1` and the output from + `ceph-disk list` looks like this:: + + /dev/sda : + /dev/sda1 other, ext2, mounted on /boot + /dev/sda2 other + /dev/sda5 other, LVM2_member + /dev/sdb : + /dev/sdb1 ceph data, active, cluster ceph, osd.1, journal /dev/sdb2 + /dev/sdb2 ceph journal, for /dev/sdb1 + /dev/sr0 other, unknown + /dev/sr1 other, unknown + + Then `/dev/sdb1` would be the right mount point. We piggy back like this + because ceph-disk does *a lot* to properly calculate those values and we + don't want to re-implement all the helpers for this. + + :param output: A list of lines from stdout + :param osd_name: The actual osd name, like `osd.1` + """ + for line in output: + line_parts = re.split(r'[,\s]+', line) + for part in line_parts: + mount_point = line_parts[1] + if osd_name == part: + return mount_point + + +def print_osd(logger, hostname, osd_path, json_blob, metadata, journal=None): + """ + A helper to print OSD metadata + """ + logger.info('-'*40) + logger.info('%s' % osd_path.split('/')[-1]) + logger.info('-'*40) + logger.info('%-14s %s' % ('Path', osd_path)) + logger.info('%-14s %s' % ('ID', json_blob.get('id'))) + logger.info('%-14s %s' % ('Name', json_blob.get('name'))) + logger.info('%-14s %s' % ('Status', json_blob.get('status'))) + logger.info('%-14s %s' % ('Reweight', json_blob.get('reweight'))) + if journal: + logger.info('Journal: %s' % journal) + for k, v in metadata.items(): + logger.info("%-13s %s" % (k.capitalize(), v)) + + logger.info('-'*40) + + +def osd(args): + cfg = conf.ceph.load(args) + + if args.subcommand == 'list': + osd_list(args, cfg) + elif args.subcommand == 'prepare': + prepare(args, cfg, activate_prepared_disk=False) + elif args.subcommand == 'create': + prepare(args, cfg, activate_prepared_disk=True) + elif args.subcommand == 'activate': + activate(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 == 'prepare': + prepare(args, cfg, activate_prepared_disk=False) + elif args.subcommand == 'activate': + activate(args, cfg) + elif args.subcommand == 'zap': + disk_zap(args) + else: + LOG.error('subcommand %s not implemented', args.subcommand) + sys.exit(1) + + +def colon_separated(s): + journal = None + disk = None + host = None + if s.count(':') == 2: + (host, disk, journal) = s.split(':') + elif s.count(':') == 1: + (host, disk) = s.split(':') + elif s.count(':') == 0: + (host) = s + else: + raise argparse.ArgumentTypeError('must be in form HOST:DISK[:JOURNAL]') + + if disk: + # allow just "sdb" to mean /dev/sdb + disk = os.path.join('/dev', disk) + if journal is not None: + journal = os.path.join('/dev', journal) + + return (host, disk, journal) + + +@priority(50) +def make(parser): + """ + Prepare a data disk on remote host. + """ + sub_command_help = dedent(""" + Manage OSDs by preparing a data disk on remote host. + + For paths, first prepare and then activate: + + ceph-deploy osd prepare {osd-node-name}:/path/to/osd + ceph-deploy osd activate {osd-node-name}:/path/to/osd + + For disks or journals the `create` command will do prepare and activate + for you. + """ + ) + parser.formatter_class = argparse.RawDescriptionHelpFormatter + parser.description = sub_command_help + + parser.add_argument( + 'subcommand', + metavar='SUBCOMMAND', + choices=[ + 'list', + 'create', + 'prepare', + 'activate', + ], + help='list, create (prepare+activate), prepare, or activate', + ) + parser.add_argument( + 'disk', + nargs='+', + metavar='HOST:DISK[:JOURNAL]', + type=colon_separated, + help='host and disk to prepare', + ) + parser.add_argument( + '--zap-disk', + action='store_true', default=None, + help='destroy existing partition table and content for DISK', + ) + parser.add_argument( + '--fs-type', + metavar='FS_TYPE', + default='xfs', + help='filesystem to use to format DISK (xfs, btrfs or ext4)', + ) + parser.add_argument( + '--dmcrypt', + action='store_true', default=None, + help='use dm-crypt on DISK', + ) + parser.add_argument( + '--dmcrypt-key-dir', + metavar='KEYDIR', + default='/etc/ceph/dmcrypt-keys', + help='directory where dm-crypt keys are stored', + ) + parser.set_defaults( + func=osd, + ) + + +@priority(50) +def make_disk(parser): + """ + Manage disks on a remote host. + """ + parser.add_argument( + 'subcommand', + metavar='SUBCOMMAND', + choices=[ + 'list', + 'prepare', + 'activate', + 'zap', + ], + help='list, prepare, activate, zap', + ) + parser.add_argument( + 'disk', + nargs='+', + metavar='HOST:DISK', + type=colon_separated, + help='host and disk (or path)', + ) + parser.add_argument( + '--zap-disk', + action='store_true', default=None, + help='destroy existing partition table and content for DISK', + ) + parser.add_argument( + '--fs-type', + metavar='FS_TYPE', + default='xfs', + help='filesystem to use to format DISK (xfs, btrfs or ext4)' + ) + parser.add_argument( + '--dmcrypt', + action='store_true', default=None, + help='use dm-crypt on DISK', + ) + parser.add_argument( + '--dmcrypt-key-dir', + metavar='KEYDIR', + default='/etc/ceph/dmcrypt-keys', + help='directory where dm-crypt keys are stored', + ) + parser.set_defaults( + func=disk, + ) diff --git a/ceph_deploy/pkg.py b/ceph_deploy/pkg.py new file mode 100644 index 0000000..d0ec93a --- /dev/null +++ b/ceph_deploy/pkg.py @@ -0,0 +1,74 @@ +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) + distro.pkg.install(distro, 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) + distro.pkg.remove(distro, 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. + """ + + parser.add_argument( + '--install', + nargs='?', + metavar='PKG(s)', + help='Comma-separated package(s) to install', + ) + + parser.add_argument( + '--remove', + nargs='?', + 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/rgw.py b/ceph_deploy/rgw.py new file mode 100644 index 0000000..ce02aa9 --- /dev/null +++ b/ceph_deploy/rgw.py @@ -0,0 +1,208 @@ +from cStringIO import StringIO +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 file(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") + + +def rgw_create(args): + cfg = conf.ceph.load(args) + LOG.debug( + 'Deploying rgw, cluster %s hosts %s', + args.cluster, + ' '.join(':'.join(x or '' for x in t) for t in args.rgw), + ) + + if not args.rgw: + raise exc.NeedHostError() + + 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) + conf_data = StringIO() + cfg.write(conf_data) + distro.conn.remote_module.write_conf( + args.cluster, + conf_data.getvalue(), + 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() + 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): + """ + Deploy ceph RGW on remote hosts. + """ + parser.add_argument( + 'subcommand', + metavar='SUBCOMMAND', + choices=[ + 'create', + ], + help='create an RGW instance', + ) + parser.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..819fc34 --- /dev/null +++ b/ceph_deploy/tests/conftest.py @@ -0,0 +1,98 @@ +import logging +import os +import subprocess +import sys + + +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) + + +def pytest_funcarg__cli(request): + """ + 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 + tmpdir = request.getfuncargvalue('tmpdir') + + 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..b36b1cd --- /dev/null +++ b/ceph_deploy/tests/fakes.py @@ -0,0 +1,27 @@ +from mock import MagicMock + + +def fake_getaddrinfo(*a, **kw): + return_host = kw.get('return_host', 'host1') + return [[0,0,0,0, return_host]] + + +def mock_open(mock=None, data=None): + """ + Fake the behavior of `open` when used as a context manager + """ + if mock is None: + mock = MagicMock(spec=file) + + handle = MagicMock(spec=file) + handle.write.return_value = None + if data is None: + handle.__enter__.return_value = handle + else: + handle.__enter__.return_value = data + mock.return_value = handle + return mock + + +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..7b9be20 --- /dev/null +++ b/ceph_deploy/tests/parser/test_admin.py @@ -0,0 +1,32 @@ +import pytest + +from ceph_deploy.cli import get_parser + + +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 "error: too few arguments" in 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_calamari.py b/ceph_deploy/tests/parser/test_calamari.py new file mode 100644 index 0000000..0a58d57 --- /dev/null +++ b/ceph_deploy/tests/parser/test_calamari.py @@ -0,0 +1,48 @@ +import pytest + +from ceph_deploy.cli import get_parser + + +class TestParserCalamari(object): + + def setup(self): + self.parser = get_parser() + + def test_calamari_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('calamari --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy calamari' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_calamari_connect_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('calamari connect --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy calamari connect' in out + assert 'positional arguments:' in out + assert 'optional arguments:' in out + + def test_calamari_connect_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('calamari connect'.split()) + out, err = capsys.readouterr() + assert "error: too few arguments" in err + + def test_calamari_connect_one_host(self): + args = self.parser.parse_args('calamari connect host1'.split()) + assert args.hosts == ['host1'] + + def test_calamari_connect_multiple_hosts(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('calamari connect'.split() + hostnames) + assert args.hosts == hostnames + + def test_calamari_connect_master_default_is_none(self): + args = self.parser.parse_args('calamari connect host1'.split()) + assert args.master is None + + def test_calamari_connect_master_custom(self): + args = self.parser.parse_args('calamari connect --master master.ceph.com host1'.split()) + assert args.master == "master.ceph.com" diff --git a/ceph_deploy/tests/parser/test_config.py b/ceph_deploy/tests/parser/test_config.py new file mode 100644 index 0000000..d71461e --- /dev/null +++ b/ceph_deploy/tests/parser/test_config.py @@ -0,0 +1,61 @@ +import pytest + +from ceph_deploy.cli import get_parser + +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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12150") + def test_config_push_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('config push'.split()) + out, err = capsys.readouterr() + assert "error: too few arguments" in 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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12150") + def test_config_pull_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('config pull'.split()) + out, err = capsys.readouterr() + assert "error: too few arguments" in 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..2483192 --- /dev/null +++ b/ceph_deploy/tests/parser/test_disk.py @@ -0,0 +1,166 @@ +import pytest + +from ceph_deploy.cli import get_parser + +SUBCMDS_WITH_ARGS = ['list', 'prepare', 'activate', '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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + 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' in err + + def test_disk_list_single_host(self): + args = self.parser.parse_args('disk list host1'.split()) + assert args.disk[0][0] == 'host1' + + def test_disk_list_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('disk list'.split() + hostnames) + # args.disk is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.disk] + assert hosts == hostnames + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + def test_disk_prepare_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk prepare --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy disk prepare' in out + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12168") + def test_disk_prepare_zap_default_false(self): + args = self.parser.parse_args('disk prepare host1:sdb'.split()) + assert args.zap_disk is False + + def test_disk_prepare_zap_true(self): + args = self.parser.parse_args('disk prepare --zap-disk host1:sdb'.split()) + assert args.zap_disk is True + + def test_disk_prepare_fstype_default_xfs(self): + args = self.parser.parse_args('disk prepare host1:sdb'.split()) + assert args.fs_type == "xfs" + + def test_disk_prepare_fstype_ext4(self): + args = self.parser.parse_args('disk prepare --fs-type ext4 host1:sdb'.split()) + assert args.fs_type == "ext4" + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + def test_disk_prepare_fstype_invalid(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk prepare --fs-type bork host1:sdb'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12168") + def test_disk_prepare_dmcrypt_default_false(self): + args = self.parser.parse_args('disk prepare host1:sdb'.split()) + assert args.dmcrypt is False + + def test_disk_prepare_dmcrypt_true(self): + args = self.parser.parse_args('disk prepare --dmcrypt host1:sdb'.split()) + assert args.dmcrypt is True + + def test_disk_prepare_dmcrypt_key_dir_default(self): + args = self.parser.parse_args('disk prepare host1:sdb'.split()) + assert args.dmcrypt_key_dir == "/etc/ceph/dmcrypt-keys" + + def test_disk_prepare_dmcrypt_key_dir_custom(self): + args = self.parser.parse_args('disk prepare --dmcrypt --dmcrypt-key-dir /tmp/keys host1:sdb'.split()) + assert args.dmcrypt_key_dir == "/tmp/keys" + + def test_disk_prepare_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk prepare'.split()) + out, err = capsys.readouterr() + assert 'too few arguments' in err + + def test_disk_prepare_single_host(self): + args = self.parser.parse_args('disk prepare host1:sdb'.split()) + assert args.disk[0][0] == 'host1' + + def test_disk_prepare_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('disk prepare'.split() + [x + ":sdb" for x in hostnames]) + # args.disk is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.disk] + assert hosts == hostnames + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + def test_disk_activate_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk activate --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy disk activate' in out + + def test_disk_activate_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('disk activate'.split()) + out, err = capsys.readouterr() + assert 'too few arguments' in err + + def test_disk_activate_single_host(self): + args = self.parser.parse_args('disk activate host1:sdb1'.split()) + assert args.disk[0][0] == 'host1' + + def test_disk_activate_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('disk activate'.split() + [x + ":sdb1" for x in hostnames]) + # args.disk is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.disk] + assert hosts == hostnames + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + 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' in err + + def test_disk_zap_single_host(self): + args = self.parser.parse_args('disk zap host1:sdb'.split()) + assert args.disk[0][0] == 'host1' + + def test_disk_zap_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('disk zap'.split() + [x + ":sdb" for x in hostnames]) + # args.disk is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.disk] + assert hosts == hostnames diff --git a/ceph_deploy/tests/parser/test_gatherkeys.py b/ceph_deploy/tests/parser/test_gatherkeys.py new file mode 100644 index 0000000..c71cc30 --- /dev/null +++ b/ceph_deploy/tests/parser/test_gatherkeys.py @@ -0,0 +1,32 @@ +import pytest + +from ceph_deploy.cli import get_parser + + +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 "error: too few arguments" in 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..df94de5 --- /dev/null +++ b/ceph_deploy/tests/parser/test_install.py @@ -0,0 +1,159 @@ +import pytest + +from ceph_deploy.cli import get_parser + +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 "error: too few arguments" in 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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12147") + 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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12147") + 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..1993c84 --- /dev/null +++ b/ceph_deploy/tests/parser/test_main.py @@ -0,0 +1,109 @@ +import pytest + +import ceph_deploy +from ceph_deploy.cli import get_parser + +SUBCMDS_WITH_ARGS = [ + 'new', 'install', 'rgw', 'mds', 'mon', 'gatherkeys', 'disk', 'osd', + 'admin', 'config', 'uninstall', 'purgedata', 'purge', 'pkg', 'calamari' +] +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 err.strip() == ceph_deploy.__version__ + + 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_custom_cluster_name(self): + args = self.parser.parse_args('--cluster myhugecluster forgetkeys'.split()) + assert args.cluster == 'myhugecluster' + + def test_custom_cluster_name_bad(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('--cluster=/evil-this-should-not-be-created'.split()) + out, err = capsys.readouterr() + assert ('--cluster: argument must start with a letter and contain only ' + 'letters and numbers') in err + + 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' in 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..5f68d84 --- /dev/null +++ b/ceph_deploy/tests/parser/test_mds.py @@ -0,0 +1,35 @@ +import pytest + +from ceph_deploy.cli import get_parser + + +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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12150") + def test_mds_create_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('mds create'.split()) + out, err = capsys.readouterr() + assert "error: too few arguments" in 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..bece634 --- /dev/null +++ b/ceph_deploy/tests/parser/test_mon.py @@ -0,0 +1,128 @@ +import pytest + +from ceph_deploy.cli import get_parser + +SUBCMDS_WITH_ARGS = ['add', 'destroy'] +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.skipif(reason="http://tracker.ceph.com/issues/12150") + @pytest.mark.parametrize('cmd', SUBCMDS_WITH_ARGS) + def test_mon_valid_subcommands_with_args(self, cmd, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args(['mon'] + ['%s' % cmd] + ['host1']) + out, err = capsys.readouterr() + assert 'too few arguments' in err + assert 'invalid choice' not in err + + @pytest.mark.parametrize('cmd', SUBCMDS_WITHOUT_ARGS) + def test_mon_valid_subcommands_without_args(self, cmd, capsys): + self.parser.parse_args(['mon'] + ['%s' % 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' + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12150") + 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"] + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12150") + 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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12150") + 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..7269e3c --- /dev/null +++ b/ceph_deploy/tests/parser/test_new.py @@ -0,0 +1,83 @@ +import pytest +from mock import patch + +from ceph_deploy.cli import get_parser +from ceph_deploy.tests.fakes import fake_arg_val_hostname + +@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 "error: too few arguments" in 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..ddd35e6 --- /dev/null +++ b/ceph_deploy/tests/parser/test_osd.py @@ -0,0 +1,207 @@ +import pytest + +from ceph_deploy.cli import get_parser + +SUBCMDS_WITH_ARGS = ['list', 'create', 'prepare', 'activate'] + + +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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + 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' in err + + def test_osd_list_single_host(self): + args = self.parser.parse_args('osd list host1'.split()) + assert args.disk[0][0] == 'host1' + + def test_osd_list_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('osd list'.split() + hostnames) + # args.disk is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.disk] + assert hosts == hostnames + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + 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_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd create'.split()) + out, err = capsys.readouterr() + assert 'too few arguments' in err + + def test_osd_create_single_host(self): + args = self.parser.parse_args('osd create host1:sdb'.split()) + assert args.disk[0][0] == 'host1' + + def test_osd_create_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('osd create'.split() + [x + ":sdb" for x in hostnames]) + # args.disk is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.disk] + assert hosts == hostnames + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12168") + def test_osd_create_zap_default_false(self): + args = self.parser.parse_args('osd create host1: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:sdb'.split()) + assert args.zap_disk is True + + def test_osd_create_fstype_default_xfs(self): + args = self.parser.parse_args('osd create host1:sdb'.split()) + assert args.fs_type == "xfs" + + def test_osd_create_fstype_ext4(self): + args = self.parser.parse_args('osd create --fs-type ext4 host1:sdb'.split()) + assert args.fs_type == "ext4" + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + def test_osd_create_fstype_invalid(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd create --fs-type bork host1:sdb'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12168") + def test_osd_create_dmcrypt_default_false(self): + args = self.parser.parse_args('osd create host1:sdb'.split()) + assert args.dmcrypt is False + + def test_osd_create_dmcrypt_true(self): + args = self.parser.parse_args('osd create --dmcrypt host1:sdb'.split()) + assert args.dmcrypt is True + + def test_osd_create_dmcrypt_key_dir_default(self): + args = self.parser.parse_args('osd create host1: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:sdb'.split()) + assert args.dmcrypt_key_dir == "/tmp/keys" + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + def test_osd_prepare_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd prepare --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy osd prepare' in out + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12168") + def test_osd_prepare_zap_default_false(self): + args = self.parser.parse_args('osd prepare host1:sdb'.split()) + assert args.zap_disk is False + + def test_osd_prepare_zap_true(self): + args = self.parser.parse_args('osd prepare --zap-disk host1:sdb'.split()) + assert args.zap_disk is True + + def test_osd_prepare_fstype_default_xfs(self): + args = self.parser.parse_args('osd prepare host1:sdb'.split()) + assert args.fs_type == "xfs" + + def test_osd_prepare_fstype_ext4(self): + args = self.parser.parse_args('osd prepare --fs-type ext4 host1:sdb'.split()) + assert args.fs_type == "ext4" + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + def test_osd_prepare_fstype_invalid(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd prepare --fs-type bork host1:sdb'.split()) + out, err = capsys.readouterr() + assert 'invalid choice' in err + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12168") + def test_osd_prepare_dmcrypt_default_false(self): + args = self.parser.parse_args('osd prepare host1:sdb'.split()) + assert args.dmcrypt is False + + def test_osd_prepare_dmcrypt_true(self): + args = self.parser.parse_args('osd prepare --dmcrypt host1:sdb'.split()) + assert args.dmcrypt is True + + def test_osd_prepare_dmcrypt_key_dir_default(self): + args = self.parser.parse_args('osd prepare host1:sdb'.split()) + assert args.dmcrypt_key_dir == "/etc/ceph/dmcrypt-keys" + + def test_osd_prepare_dmcrypt_key_dir_custom(self): + args = self.parser.parse_args('osd prepare --dmcrypt --dmcrypt-key-dir /tmp/keys host1:sdb'.split()) + assert args.dmcrypt_key_dir == "/tmp/keys" + + def test_osd_prepare_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd prepare'.split()) + out, err = capsys.readouterr() + assert 'too few arguments' in err + + def test_osd_prepare_single_host(self): + args = self.parser.parse_args('osd prepare host1:sdb'.split()) + assert args.disk[0][0] == 'host1' + + def test_osd_prepare_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('osd prepare'.split() + [x + ":sdb" for x in hostnames]) + # args.disk is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.disk] + assert hosts == hostnames + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12151") + def test_osd_activate_help(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd activate --help'.split()) + out, err = capsys.readouterr() + assert 'usage: ceph-deploy osd activate' in out + + def test_osd_activate_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('osd activate'.split()) + out, err = capsys.readouterr() + assert 'too few arguments' in err + + def test_osd_activate_single_host(self): + args = self.parser.parse_args('osd activate host1:sdb1'.split()) + assert args.disk[0][0] == 'host1' + + def test_osd_activate_multi_host(self): + hostnames = ['host1', 'host2', 'host3'] + args = self.parser.parse_args('osd activate'.split() + [x + ":sdb1" for x in hostnames]) + # args.disk is a list of tuples, and tuple[0] is the hostname + hosts = [x[0] for x in args.disk] + assert hosts == hostnames diff --git a/ceph_deploy/tests/parser/test_purge.py b/ceph_deploy/tests/parser/test_purge.py new file mode 100644 index 0000000..a241892 --- /dev/null +++ b/ceph_deploy/tests/parser/test_purge.py @@ -0,0 +1,32 @@ +import pytest + +from ceph_deploy.cli import get_parser + + +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 "error: too few arguments" in 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..4d30fa8 --- /dev/null +++ b/ceph_deploy/tests/parser/test_purgedata.py @@ -0,0 +1,32 @@ +import pytest + +from ceph_deploy.cli import get_parser + + +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 "error: too few arguments" in 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_rgw.py b/ceph_deploy/tests/parser/test_rgw.py new file mode 100644 index 0000000..3cd0f6c --- /dev/null +++ b/ceph_deploy/tests/parser/test_rgw.py @@ -0,0 +1,35 @@ +import pytest + +from ceph_deploy.cli import get_parser + + +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 + + @pytest.mark.skipif(reason="http://tracker.ceph.com/issues/12150") + def test_rgw_create_host_required(self, capsys): + with pytest.raises(SystemExit): + self.parser.parse_args('rgw create'.split()) + out, err = capsys.readouterr() + assert "error: too few arguments" in 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..81fc70c --- /dev/null +++ b/ceph_deploy/tests/parser/test_uninstall.py @@ -0,0 +1,32 @@ +import pytest + +from ceph_deploy.cli import get_parser + + +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 "error: too few arguments" in 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..aeb0778 --- /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() + 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() + 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('w'): + pass + + etc_ceph = os.path.join(str(tmpdir), 'etc', 'ceph') + os.makedirs(etc_ceph) + + distro = MagicMock() + distro.conn = MagicMock() + remotes.write_file.func_defaults = (str(tmpdir),) + 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 & 0777) + assert file_mode == oct(0600) diff --git a/ceph_deploy/tests/test_cli_mon.py b/ceph_deploy/tests/test_cli_mon.py new file mode 100644 index 0000000..f90cf8d --- /dev/null +++ b/ceph_deploy/tests/test_cli_mon.py @@ -0,0 +1,78 @@ +import argparse +import collections +import subprocess + +import pytest +from mock import Mock, patch, NonCallableMock + +from ceph_deploy.cli import _main as main +from ceph_deploy.tests.directory import directory + + +#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() + assert 'usage: ceph-deploy' in result + assert 'too few arguments' in 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_simple(tmpdir, capsys): + with tmpdir.join('ceph.conf').open('w') as f: + f.write("""\ +[global] +fsid = 6ede5564-3cf1-44b5-aa96-1c77b0c3e1d0 +mon initial members = host1 +""") + + ns = argparse.Namespace() + ns.pushy = Mock() + conn = NonCallableMock(name='PushyClient') + ns.pushy.return_value = conn + + mock_compiled = collections.defaultdict(Mock) + conn.compile.side_effect = mock_compiled.__getitem__ + + MON_SECRET = 'AQBWDj5QAP6LHhAAskVBnUkYHJ7eYREmKo5qKA==' + + def _create_mon(cluster, get_monitor_secret): + secret = get_monitor_secret() + assert secret == MON_SECRET + + 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( + args=['-v', 'new', '--no-ssh-copykey', 'host1'], + namespace=ns, + ) + main( + args=['-v', 'mon', 'create', 'host1'], + namespace=ns, + ) + 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..cdd2ae6 --- /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 + + +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'] + + +def pytest_funcarg__newcfg(request): + tmpdir = request.getfuncargvalue('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' + assert cfg.get('global', 'filestore_xattr_use_omap') == 'true' 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..4a51bf5 --- /dev/null +++ b/ceph_deploy/tests/test_conf.py @@ -0,0 +1,68 @@ +from cStringIO 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.reset() + assert f.readlines() == ['[foo]\n', 'bar_thud_quux = baz\n','\n'] diff --git a/ceph_deploy/tests/test_install.py b/ceph_deploy/tests/test_install.py new file mode 100644 index 0000000..abd1fe7 --- /dev/null +++ b/ceph_deploy/tests/test_install.py @@ -0,0 +1,113 @@ +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_mon = False + self.args.install_osd = False + self.args.install_rgw = 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 + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph-osd', 'ceph-mds', '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.args.install_all = True + self.args.install_mds = 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-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-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_osd = True + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted([ + 'ceph-osd', 'ceph-mds', 'ceph-mon', 'ceph-radosgw' + ]) + + 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 + result = sorted(install.detect_components(self.args, self.distro)) + assert result == sorted(['ceph-osd', 'ceph-mds']) + + 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-mon', 'ceph-radosgw' + ]) diff --git a/ceph_deploy/tests/test_mon.py b/ceph_deploy/tests/test_mon.py new file mode 100644 index 0000000..11560fe --- /dev/null +++ b/ceph_deploy/tests/test_mon.py @@ -0,0 +1,92 @@ +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.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) + 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..7138429 --- /dev/null +++ b/ceph_deploy/tests/test_remotes.py @@ -0,0 +1,85 @@ +from mock import patch +from ceph_deploy.hosts import remotes +from ceph_deploy.hosts.remotes import platform_information + +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' 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_hosts.py b/ceph_deploy/tests/unit/hosts/test_hosts.py new file mode 100644 index 0000000..c9d9c63 --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_hosts.py @@ -0,0 +1,409 @@ +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' + + +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_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') 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..ca428d0 --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_remotes.py @@ -0,0 +1,16 @@ +from cStringIO 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 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..bd72d22 --- /dev/null +++ b/ceph_deploy/tests/unit/hosts/test_suse.py @@ -0,0 +1,25 @@ +from ceph_deploy.hosts import suse + +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() + assert init_type == "sysvinit" + + def test_choose_init_SLE_11(self): + self.host.release = '11' + init_type = self.host.choose_init() + assert init_type == "sysvinit" + + def test_choose_init_SLE_12(self): + self.host.release = '12' + init_type = self.host.choose_init() + assert init_type == "systemd" + + def test_choose_init_openSUSE_13_1(self): + self.host.release = '13.1' + init_type = self.host.choose_init() + assert init_type == "systemd" 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_calamari.py b/ceph_deploy/tests/unit/test_calamari.py new file mode 100644 index 0000000..ce85bfd --- /dev/null +++ b/ceph_deploy/tests/unit/test_calamari.py @@ -0,0 +1,17 @@ +import pytest +from ceph_deploy import calamari + + +class TestDistroIsSupported(object): + + @pytest.mark.parametrize( + "distro_name", + ['centos', 'redhat', 'ubuntu', 'debian']) + def test_distro_is_supported(self, distro_name): + assert calamari.distro_is_supported(distro_name) is True + + @pytest.mark.parametrize( + "distro_name", + ['fedora', 'mandriva', 'darwin', 'windows']) + def test_distro_is_not_supported(self, distro_name): + assert calamari.distro_is_supported(distro_name) is False 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..7bdc4fb --- /dev/null +++ b/ceph_deploy/tests/unit/test_conf.py @@ -0,0 +1,192 @@ +from cStringIO import StringIO +from textwrap import dedent +import pytest +from mock import Mock, patch +from ceph_deploy import conf +from ceph_deploy.tests import fakes + + +class TestLocateOrCreate(object): + + def setup(self): + self.fake_write = Mock(name='fake_write') + self.fake_file = fakes.mock_open(data=self.fake_write) + self.fake_file.readline.return_value = self.fake_file + + def test_no_conf(self): + fake_path = Mock() + fake_path.exists = Mock(return_value=False) + with patch('__builtin__.open', self.fake_file): + 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.readfp(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.readfp(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.readfp(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.readfp(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.readfp(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.readfp(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.readfp(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.readfp(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..0cfccba --- /dev/null +++ b/ceph_deploy/tests/unit/test_mon.py @@ -0,0 +1,225 @@ +import sys +import py.test +from mock import Mock, patch, call +from ceph_deploy import mon +from ceph_deploy.tests import fakes +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_write = Mock(name='fake_write') + self.fake_file = fakes.mock_open(data=self.fake_write) + self.fake_file.readline.return_value = self.fake_file + self.fake_file.readline.lstrip.return_value = '' + 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 == 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 = [call.get('name1', 'name1'), + call.get('name2', 'name2.localdomain'), + call.get('name3', '1.2.3.6'), + 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/test_osd.py b/ceph_deploy/tests/unit/test_osd.py new file mode 100644 index 0000000..d830f06 --- /dev/null +++ b/ceph_deploy/tests/unit/test_osd.py @@ -0,0 +1,74 @@ +from mock import Mock +import string +from ceph_deploy import osd + + +class TestMountPoint(object): + + def setup(self): + self.osd_name = 'osd.1' + + def test_osd_name_not_found(self): + output = [ + '/dev/sda :', + ' /dev/sda1 other, ext2, mounted on /boot', + ' /dev/sda2 other', + ' /dev/sda5 other, LVM2_member', + ] + assert osd.get_osd_mount_point(output, self.osd_name) is None + + def test_osd_name_is_found(self): + output = [ + '/dev/sda :', + ' /dev/sda1 other, ext2, mounted on /boot', + ' /dev/sda2 other', + ' /dev/sda5 other, LVM2_member', + '/dev/sdb :', + ' /dev/sdb1 ceph data, active, cluster ceph, osd.1, journal /dev/sdb2', + ] + result = osd.get_osd_mount_point(output, self.osd_name) + assert result == '/dev/sdb1' + + def test_osd_name_not_found_but_contained_in_output(self): + output = [ + '/dev/sda :', + ' /dev/sda1 otherosd.1, ext2, mounted on /boot', + ' /dev/sda2 other', + ' /dev/sda5 other, LVM2_member', + ] + assert osd.get_osd_mount_point(output, self.osd_name) is None + + +class TestOsdPerHostCheck(object): + + def setup(self): + self.args = Mock() + self.args.disk = [ + ('node1', '/dev/sdb'), + ('node2', '/dev/sdb'), + ('node3', '/dev/sdb'), + ] + self.disks = ['/dev/sd%s' % disk for disk in list(string.ascii_lowercase)] + + def test_no_journal_works(self): + assert osd.exceeds_max_osds(self.args) == {} + + def test_mixed_journal_and_no_journal_works(self): + self.args.disk = [ + ('node1', '/dev/sdb'), + ('node2', '/dev/sdb', '/dev/sdc'), + ('node3', '/dev/sdb'), + ] + assert osd.exceeds_max_osds(self.args) == {} + + def test_minimum_count_passes(self): + self.args.disk = [ + ('node1', '/dev/sdb'), + ('node2', '/dev/sdb'), + ('node3', '/dev/sdb'), + ] + assert osd.exceeds_max_osds(self.args) == {} + + def test_exceeds_reasonable(self): + self.args.disk = [('node1', disk) for disk in self.disks] + assert osd.exceeds_max_osds(self.args) == {'node1': 26} 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..690201c --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_net.py @@ -0,0 +1,32 @@ +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 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..1f95dad --- /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://git.ceph.com/?p=ceph.git;a=blob_plain;f=keys/release.asc" + + def test_gpg_url_autobuild(self): + result = paths.gpg.url('autobuild') + assert result == "https://git.ceph.com/?p=ceph.git;a=blob_plain;f=keys/autobuild.asc" + + def test_gpg_url_http(self): + result = paths.gpg.url('release', protocol="http") + assert result == "http://git.ceph.com/?p=ceph.git;a=blob_plain;f=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..f4ebb02 --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_pkg_managers.py @@ -0,0 +1,139 @@ +from mock import patch, Mock +from ceph_deploy.util import pkg_managers + + +class TestRPM(object): + + def setup(self): + self.to_patch = 'ceph_deploy.util.pkg_managers.remoto.process.run' + + def test_normal_flags(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.rpm(Mock()) + result = fake_run.call_args_list[-1] + assert result[0][-1] == ['rpm', '-Uvh'] + + def test_extended_flags(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.rpm( + Mock(), + ['-f', 'vim']) + result = fake_run.call_args_list[-1] + assert result[0][-1] == ['rpm', '-Uvh', '-f', 'vim'] + + +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(), '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(), ['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_remove(Mock(), '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_remove(Mock(), ['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(), '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(), ['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_remove(Mock(), '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_remove(Mock(), ['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' + + def test_install_single_package(self): + fake_run = Mock() + with patch(self.to_patch, fake_run): + pkg_managers.zypper(Mock(), '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(), ['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.zypper_remove(Mock(), '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.zypper_remove(Mock(), ['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..d4fc11d --- /dev/null +++ b/ceph_deploy/tests/unit/util/test_system.py @@ -0,0 +1,19 @@ +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') 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..2513de6 --- /dev/null +++ b/ceph_deploy/tests/util.py @@ -0,0 +1,28 @@ + + +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) diff --git a/ceph_deploy/util/__init__.py b/ceph_deploy/util/__init__.py new file mode 100644 index 0000000..e69de29 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..2acdd1e --- /dev/null +++ b/ceph_deploy/util/constants.py @@ -0,0 +1,32 @@ +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') + +mds_path = join(base_path, 'mds') + +osd_path = join(base_path, 'osd') + +# Default package components to install +_base_components = [ + 'ceph-osd', + 'ceph-mds', + 'ceph-mon', +] + +default_components = namedtuple('DefaultComponents', ['rpm', 'deb']) + +# 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']) + +gpg_key_base_url = "git.ceph.com/?p=ceph.git;a=blob_plain;f=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..9e5e702 --- /dev/null +++ b/ceph_deploy/util/help_formatters.py @@ -0,0 +1,11 @@ +import argparse + + +class ToggleRawTextHelpFormatter(argparse.HelpFormatter): + """Inspired by the SmartFormatter at + https://bitbucket.org/ruamel/std.argparse + """ + 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..6a298cf --- /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" + + +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..84ba18d --- /dev/null +++ b/ceph_deploy/util/net.py @@ -0,0 +1,363 @@ +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 addresses assigned to the host. 127.0.0.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'] + + """ + ret = set() + ifaces = linux_interfaces(conn) + if interface is None: + target_ifaces = ifaces + else: + target_ifaces = dict([(k, v) for k, v in ifaces.iteritems() + if k == interface]) + if not target_ifaces: + LOG.error('Interface {0} not found.'.format(interface)) + for ipv4_info in target_ifaces.values(): + for ipv4 in ipv4_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 ipv4_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) + 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 diff --git a/ceph_deploy/util/paths/__init__.py b/ceph_deploy/util/paths/__init__.py new file mode 100644 index 0000000..45095cb --- /dev/null +++ b/ceph_deploy/util/paths/__init__.py @@ -0,0 +1,3 @@ +import mon # noqa +import osd # noqa +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..8985112 --- /dev/null +++ b/ceph_deploy/util/pkg_managers.py @@ -0,0 +1,166 @@ +from ceph_deploy.lib import remoto + + +def apt(conn, packages, *a, **kw): + if isinstance(packages, str): + packages = [packages] + cmd = [ + 'env', + 'DEBIAN_FRONTEND=noninteractive', + 'apt-get', + 'install', + '--assume-yes', + ] + cmd.extend(packages) + return remoto.process.run( + conn, + cmd, + *a, + **kw + ) + + +def apt_remove(conn, packages, *a, **kw): + if isinstance(packages, str): + packages = [packages] + + purge = kw.pop('purge', False) + cmd = [ + 'apt-get', + '-q', + 'remove', + '-f', + '-y', + '--force-yes', + ] + if purge: + cmd.append('--purge') + cmd.extend(packages) + + return remoto.process.run( + conn, + cmd, + *a, + **kw + ) + + +def apt_update(conn): + cmd = [ + 'apt-get', + '-q', + 'update', + ] + return remoto.process.run( + conn, + cmd, + ) + + +def yum(conn, packages, *a, **kw): + if isinstance(packages, str): + packages = [packages] + + cmd = [ + 'yum', + '-y', + 'install', + ] + cmd.extend(packages) + return remoto.process.run( + conn, + cmd, + *a, + **kw + ) + + +def yum_remove(conn, packages, *a, **kw): + cmd = [ + 'yum', + '-y', + '-q', + 'remove', + ] + if isinstance(packages, str): + cmd.append(packages) + else: + cmd.extend(packages) + return remoto.process.run( + conn, + cmd, + *a, + **kw + ) + + +def yum_clean(conn, item=None): + item = item or 'all' + cmd = [ + 'yum', + 'clean', + item, + ] + + return remoto.process.run( + conn, + cmd, + ) + + +def rpm(conn, rpm_args=None, *a, **kw): + """ + A minimal front end for ``rpm`. Extra flags can be passed in via + ``rpm_args`` as an iterable. + """ + rpm_args = rpm_args or [] + cmd = [ + 'rpm', + '-Uvh', + ] + cmd.extend(rpm_args) + return remoto.process.run( + conn, + cmd, + *a, + **kw + ) + + +def zypper(conn, packages, *a, **kw): + if isinstance(packages, str): + packages = [packages] + + cmd = [ + 'zypper', + '--non-interactive', + 'install', + ] + + cmd.extend(packages) + return remoto.process.run( + conn, + cmd, + *a, + **kw + ) + + +def zypper_remove(conn, packages, *a, **kw): + cmd = [ + 'zypper', + '--non-interactive', + '--quiet', + 'remove', + ] + + if isinstance(packages, str): + cmd.append(packages) + else: + cmd.extend(packages) + return remoto.process.run( + conn, + cmd, + *a, + **kw + ) diff --git a/ceph_deploy/util/ssh.py b/ceph_deploy/util/ssh.py new file mode 100644 index 0000000..2a5b9e2 --- /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.connection.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] + 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..a8a38bd --- /dev/null +++ b/ceph_deploy/util/system.py @@ -0,0 +1,58 @@ +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 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', + ] + ) diff --git a/ceph_deploy/util/templates.py b/ceph_deploy/util/templates.py new file mode 100644 index 0000000..7c064da --- /dev/null +++ b/ceph_deploy/util/templates.py @@ -0,0 +1,95 @@ + + +ceph_repo = """ +[ceph] +name=Ceph packages for $basearch +baseurl={repo_url}/$basearch +enabled=1 +gpgcheck=1 +priority=1 +type=rpm-md +gpgkey={gpg_url} + +[ceph-noarch] +name=Ceph noarch packages +baseurl={repo_url}/noarch +enabled=1 +gpgcheck=1 +priority=1 +type=rpm-md +gpgkey={gpg_url} + +[ceph-source] +name=Ceph source packages +baseurl={repo_url}/SRPMS +enabled=0 +gpgcheck=1 +type=rpm-md +gpgkey={gpg_url} +""" + +zypper_repo = """[ceph] +name=Ceph packages +type=rpm-md +baseurl={repo_url} +gpgcheck=1 +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/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..878f3c3 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,275 @@ +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..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..d305ce5 --- /dev/null +++ b/debian/control @@ -0,0 +1,25 @@ +Source: ceph-deploy +Maintainer: Sage Weil +Uploaders: Sage Weil +Section: admin +Priority: optional +Build-Depends: debhelper (>= 7), python-setuptools, git +X-Python-Version: >= 2.6 +Standards-Version: 3.9.2 +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: python, + python-argparse, + python-setuptools, + ${misc:Depends}, + ${python: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..b46b956 --- /dev/null +++ b/debian/rules @@ -0,0 +1,12 @@ +#!/usr/bin/make -f + +# Uncomment this to turn on verbose mode. +export DH_VERBOSE=1 +@export DEB_PYTHON_INSTALL_ARGS_ALL += --install-lib=/usr/share/ceph-deploy + +%: + dh $@ --buildsystem python_distutils --with python2 + +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/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 0000000..e87a114 --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,401 @@ +Changelog +========= + +1.5 +--- + +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..b841234 --- /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..91448fa --- /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://git.ceph.com/?p=ceph.git;a=blob_plain;f=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://git.ceph.com/?p=ceph.git;a=blob_plain;f=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://git.ceph.com/?p=ceph.git;a=blob_plain;f=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..22b94c6 --- /dev/null +++ b/docs/source/contents.rst @@ -0,0 +1,15 @@ +Content Index +============= + +.. toctree:: + :maxdepth: 2 + + index.rst + new.rst + install.rst + mon.rst + rgw.rst + mds.rst + conf.rst + pkg.rst + changelog.rst diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..7f842ee --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,298 @@ +======================================================== + 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 + +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. + +bootstraping +------------ +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 + + + +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. diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 0000000..4050e9b --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,218 @@ + +.. _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 + +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://git.ceph.com/?p=ceph.git;a=blob_plain;f=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://git.ceph.com/?p=ceph.git;a=blob_plain;f=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..86f76ab --- /dev/null +++ b/docs/source/mon.rst @@ -0,0 +1,100 @@ +.. _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. + +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/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..dbc0d19 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest >=2.1.3 +tox >=1.2 +mock >=1.0b1 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..d9ec107 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[pytest] +norecursedirs = .* _* virtualenv diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..81fcecd --- /dev/null +++ b/setup.py @@ -0,0 +1,79 @@ +from setuptools import setup, find_packages +import os +import sys +import ceph_deploy +from vendor import vendorize, clean_vendor + + +def read(fname): + path = os.path.join(os.path.dirname(__file__), fname) + f = open(path) + return f.read() + +install_requires = [] +pyversion = sys.version_info[:2] +if pyversion < (2, 7) or (3, 0) <= pyversion <= (3, 1): + install_requires.append('argparse') + +# +# Add libraries that are not part of install_requires but only if we really +# want to, specified by the environment flag +# + +if os.environ.get('CEPH_DEPLOY_NO_VENDOR'): + clean_vendor('remoto') +else: + vendorize([ + ('remoto', '0.0.25', ['python', 'vendor.py']), + ]) + + +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=[ + 'setuptools', + ] + 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', + 'forgetkeys = ceph_deploy.forgetkeys:make', + 'config = ceph_deploy.config:make', + 'admin = ceph_deploy.admin:make', + 'pkg = ceph_deploy.pkg:make', + 'calamari = ceph_deploy.calamari:make', + 'rgw = ceph_deploy.rgw:make', + ], + + }, + ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..3da3548 --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +[tox] +envlist = py26, py27, flake8 + +[testenv] +deps= + pytest + mock +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 --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:py26-novendor] +sitepackages=True +deps= + +[testenv:py27-novendor] +sitepackages=True +deps= diff --git a/vendor.py b/vendor.py new file mode 100644 index 0000000..8d6ac6a --- /dev/null +++ b/vendor.py @@ -0,0 +1,110 @@ +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://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.47.3