diff options
Diffstat (limited to '')
17 files changed, 580 insertions, 0 deletions
diff --git a/src/ceph-volume/plugin/zfs/CMakeLists.txt b/src/ceph-volume/plugin/zfs/CMakeLists.txt new file mode 100644 index 000000000..da10f46fd --- /dev/null +++ b/src/ceph-volume/plugin/zfs/CMakeLists.txt @@ -0,0 +1,3 @@ + +distutils_install_module(ceph_volume_zfs + INSTALL_SCRIPT ${CMAKE_INSTALL_FULL_SBINDIR}) diff --git a/src/ceph-volume/plugin/zfs/LICENSE b/src/ceph-volume/plugin/zfs/LICENSE new file mode 100644 index 000000000..92cc048b8 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/LICENSE @@ -0,0 +1,32 @@ + + +BSD License + +Copyright (c) 2018, Willem Jan Withagen +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/src/ceph-volume/plugin/zfs/MANIFEST.in b/src/ceph-volume/plugin/zfs/MANIFEST.in new file mode 100644 index 000000000..ed96496e6 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/MANIFEST.in @@ -0,0 +1,7 @@ +include LICENSE + +recursive-include ceph_volume_zfs * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] + +recursive-include *.rst conf.py Makefile *.jpg *.png *.gif diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/__init__.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/__init__.py new file mode 100755 index 000000000..0b0889f36 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +"""Top-level package for Ceph volume on ZFS.""" + +__author__ = """Willem Jan Withagen""" +__email__ = 'wjw@digiware.nl' + +import ceph_volume_zfs.zfs + +from collections import namedtuple + +sys_info = namedtuple('sys_info', ['devices']) +sys_info.devices = dict() diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/api/__init__.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/api/__init__.py new file mode 100644 index 000000000..ecc971299 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/api/__init__.py @@ -0,0 +1,3 @@ +""" +Device API that can be shared among other implementations. +""" diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/__init__.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/__init__.py new file mode 100755 index 000000000..c1a8fe656 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import zfs diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/__init__.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/__init__.py new file mode 100755 index 000000000..457418493 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +import logging +logger = logging.getLogger(__name__) diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/inventory.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/inventory.py new file mode 100644 index 000000000..be65e39ac --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/inventory.py @@ -0,0 +1,50 @@ +import argparse +import json +from textwrap import dedent + +# import ceph_volume.process + +from ceph_volume_zfs.util.disk import Disks + +class Inventory(object): + + help = 'Generate a list of available devices' + + def __init__(self, argv): + self.argv = argv + + def format_report(self, inventory): + if self.args.format == 'json': + print(json.dumps(inventory.json_report())) + elif self.args.format == 'json-pretty': + print(json.dumps(inventory.json_report(), indent=4, sort_keys=True)) + else: + print(inventory.pretty_report()) + + def main(self): + sub_command_help = dedent(""" + Generate an inventory of available devices + """) + parser = argparse.ArgumentParser( + prog='ceph-volume zfs inventory', + description=sub_command_help, + ) + parser.add_argument( + 'path', + nargs='?', + default=None, + help=('Report on specific disk'), + ) + parser.add_argument( + '--format', + choices=['plain', 'json', 'json-pretty'], + default='plain', + help='Output format', + ) + + self.args = parser.parse_args(self.argv) + if self.args.path: + self.format_report(Disks(self.args.path)) + else: + self.format_report(Disks()) + diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/main.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/main.py new file mode 100644 index 000000000..073be6467 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/main.py @@ -0,0 +1,36 @@ +# vim: expandtab smarttab shiftwidth=4 softtabstop=4 + +import argparse +from textwrap import dedent +from ceph_volume import terminal + +from . import inventory +from . import prepare +from . import zap + +class ZFSDEV(object): + + help = 'Use ZFS to deploy OSDs' + + _help = dedent(""" + Use ZFS to deploy OSDs + + {sub_help} + """) + + def __init__(self, argv): + self.argv = argv + + def print_help(self, sub_help): + return self._help.format(sub_help=sub_help) + + def main(self): + terminal.dispatch(self.mapper, self.argv) + parser = argparse.ArgumentParser( + prog='ceph-volume zfs', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=self.print_help(terminal.subhelp(self.mapper)), + ) + parser.parse_args(self.argv) + if len(self.argv) <= 1: + return parser.print_help() diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/prepare.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/prepare.py new file mode 100644 index 000000000..7c075e86a --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/prepare.py @@ -0,0 +1,25 @@ +import argparse + +from textwrap import dedent +# from ceph_volume.util import arg_validators + +class Prepare(object): + + help = 'Prepare a device' + + def __init__(self, argv): + self.argv = argv + + def main(self): + sub_command_help = dedent(""" + Prepare a device + """) + parser = argparse.ArgumentParser( + prog='ceph-volume zfs prepare', + description=sub_command_help, + ) + if len(self.argv) == 0 or len(self.argv) > 0: + print("Prepare: Print Help") + print(sub_command_help) + return + diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/zap.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/zap.py new file mode 100644 index 000000000..f5177d5f2 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/devices/zfs/zap.py @@ -0,0 +1,34 @@ +import argparse + +from textwrap import dedent +# from ceph_volume.util import arg_validators + +class Zap(object): + + help = 'Zap a device' + + def __init__(self, argv): + self.argv = argv + + def main(self): + sub_command_help = dedent(""" + Zap a device + """) + parser = argparse.ArgumentParser( + prog='ceph-volume zfs inventory', + description=sub_command_help, + ) + parser.add_argument( + 'devices', + metavar='DEVICES', + nargs='*', + # type=arg_validators.ValidDevice(gpt_ok=True), + default=[], + help='Path to one or many lv (as vg/lv), partition (as /dev/sda1) or device (as /dev/sda)' + ) + + if len(self.argv) == 0 or len(self.argv) > 0: + print("Zap: Print Help") + print(sub_command_help) + return + diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/util/__init__.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/util/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/util/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/util/disk.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/util/disk.py new file mode 100644 index 000000000..b666aa7d5 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/util/disk.py @@ -0,0 +1,148 @@ +import re + +from ceph_volume.util.disk import human_readable_size +from ceph_volume import process +from ceph_volume import sys_info + +report_template = """ +/dev/{geomname:<16} {mediasize:<16} {rotational!s:<7} {descr}""" +# {geomname:<25} {mediasize:<12} {rotational!s:<7} {mode!s:<9} {descr}""" + +def geom_disk_parser(block): + """ + Parses lines in 'geom disk list` output. + + Geom name: ada3 + Providers: + 1. Name: ada3 + Mediasize: 40018599936 (37G) + Sectorsize: 512 + Stripesize: 4096 + Stripeoffset: 0 + Mode: r2w2e4 + descr: Corsair CSSD-F40GB2 + lunid: 5000000000000236 + ident: 111465010000101800EC + rotationrate: 0 + fwsectors: 63 + fwheads: 16 + + :param line: A string, with the full block for `geom disk list` + """ + pairs = block.split(';') + parsed = {} + for pair in pairs: + if 'Providers' in pair: + continue + try: + column, value = pair.split(':') + except ValueError: + continue + # fixup + column = re.sub("\s+", "", column) + column= re.sub("^[0-9]+\.", "", column) + value = value.strip() + value = re.sub('\([0-9A-Z]+\)', '', value) + parsed[column.lower()] = value + return parsed + +def get_disk(diskname): + """ + Captures all available info from geom + along with interesting metadata like sectors, size, vendor, + solid/rotational, etc... + + Returns a dictionary, with all the geom fields as keys. + """ + + command = ['/sbin/geom', 'disk', 'list', re.sub('/dev/', '', diskname)] + out, err, rc = process.call(command) + geom_block = "" + for line in out: + line.strip() + geom_block += ";" + line + disk = geom_disk_parser(geom_block) + return disk + +def get_disks(): + command = ['/sbin/geom', 'disk', 'status', '-s'] + out, err, rc = process.call(command) + disks = {} + for path in out: + dsk, rest1, rest2 = path.split() + disk = get_disk(dsk) + disks['/dev/'+dsk] = disk + return disks + +class Disks(object): + + def __init__(self, path=None): + if not sys_info.devices: + sys_info.devices = get_disks() + self.disks = {} + for k in sys_info.devices: + if path != None: + if path in k: + self.disks[k] = Disk(k) + else: + self.disks[k] = Disk(k) + + def pretty_report(self, all=True): + output = [ + report_template.format( + geomname='Device Path', + mediasize='Size', + rotational='rotates', + descr='Model name', + mode='available', + )] + for disk in sorted(self.disks): + output.append(self.disks[disk].report()) + return ''.join(output) + + def json_report(self): + output = [] + for disk in sorted(self.disks): + output.append(self.disks[disk].json_report()) + return output + + +class Disk(object): + + report_fields = [ + 'rejected_reasons', + 'available', + 'path', + 'sys_api', + ] + pretty_report_sys_fields = [ + 'human_readable_size', + 'model', + 'removable', + 'ro', + 'rotational', + 'sas_address', + 'scheduler_mode', + 'vendor', + ] + + def __init__(self, path): + self.abspath = path + self.path = path + self.reject_reasons = [] + self.available = True + self.sys_api = sys_info.devices.get(path) + + def report(self): + return report_template.format( + geomname=self.sys_api.get('geomname'), + mediasize=human_readable_size(int(self.sys_api.get('mediasize'))), + rotational=int(self.sys_api.get('rotationrate')) != 0, + mode=self.sys_api.get('mode'), + descr=self.sys_api.get('descr') + ) + + def json_report(self): + output = {k.strip('_'): v for k, v in vars(self).items()} + return output + diff --git a/src/ceph-volume/plugin/zfs/ceph_volume_zfs/zfs.py b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/zfs.py new file mode 100755 index 000000000..e9911c75e --- /dev/null +++ b/src/ceph-volume/plugin/zfs/ceph_volume_zfs/zfs.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function +import argparse +import os +import sys +import logging + +from textwrap import dedent +from ceph_volume import log, conf, configuration +from ceph_volume import exceptions +from ceph_volume import terminal + +# The ceph-volume-zfs specific code +import ceph_volume_zfs.zfs +from ceph_volume_zfs import devices +# from ceph_volume_zfs.util import device +from ceph_volume_zfs.devices import zfs + +# the supported actions +from ceph_volume_zfs.devices.zfs import inventory +from ceph_volume_zfs.devices.zfs import prepare +from ceph_volume_zfs.devices.zfs import zap + + +if __name__ == '__main__': + zfs.ZFS() + + +class ZFS(object): + + # help info for subcommands + help = "Use ZFS as the underlying technology for OSDs" + + # help info for the plugin + help_menu = "Deploy OSDs with ZFS" + _help = dedent(""" + Use ZFS as the underlying technology for OSDs + + {sub_zfshelp} + """) + name = 'zfs' + + def __init__(self, argv=None, parse=True): + self.zfs_mapper = { + 'inventory': inventory.Inventory, + 'prepare': prepare.Prepare, + 'zap': zap.Zap, + } + if argv is None: + self.argv = sys.argv + else: + self.argv = argv + if parse: + self.main(self.argv) + + def print_help(self, warning=False): + return self._help.format( + sub_zfshelp=terminal.subhelp(self.zfs_mapper) + ) + + def get_environ_vars(self): + environ_vars = [] + for key, value in os.environ.items(): + if key.startswith('CEPH_'): + environ_vars.append("%s=%s" % (key, value)) + if not environ_vars: + return '' + else: + environ_vars.insert(0, '\nEnviron Variables:') + return '\n'.join(environ_vars) + + def load_ceph_conf_path(self, cluster_name='ceph'): + abspath = '/etc/ceph/%s.conf' % cluster_name + conf.path = os.getenv('CEPH_CONF', abspath) + conf.cluster = cluster_name + + def stat_ceph_conf(self): + try: + configuration.load(conf.path) + return terminal.green(conf.path) + except exceptions.ConfigurationError as error: + return terminal.red(error) + + def load_log_path(self): + conf.log_path = os.getenv('CEPH_VOLUME_LOG_PATH', '/var/log/ceph') + + def _get_split_args(self): + subcommands = self.zfs_mapper.keys() + slice_on_index = len(self.argv) + pruned_args = self.argv + for count, arg in enumerate(pruned_args): + if arg in subcommands: + slice_on_index = count + break + return pruned_args[:slice_on_index], pruned_args[slice_on_index:] + + def main(self, argv=None): + if argv is None: + return + self.load_ceph_conf_path() + # these need to be available for the help, which gets parsed super + # early + self.load_ceph_conf_path() + self.load_log_path() + main_args, subcommand_args = self._get_split_args() + # no flags where passed in, return the help menu instead of waiting for + # argparse which will end up complaning that there are no args + if len(argv) < 1: + print(self.print_help(warning=True)) + return + parser = argparse.ArgumentParser( + prog='ceph-volume-zfs', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=self.print_help(), + ) + parser.add_argument( + '--cluster', + default='ceph', + help='Cluster name (defaults to "ceph")', + ) + parser.add_argument( + '--log-level', + default='debug', + help='Change the file log level (defaults to debug)', + ) + parser.add_argument( + '--log-path', + default='/var/log/ceph/', + help='Change the log path (defaults to /var/log/ceph)', + ) + args = parser.parse_args(main_args) + conf.log_path = args.log_path + if os.path.isdir(conf.log_path): + conf.log_path = os.path.join(args.log_path, 'ceph-volume-zfs.log') + log.setup() + logger = logging.getLogger(__name__) + logger.info("Running command: ceph-volume-zfs %s %s", + " ".join(main_args), " ".join(subcommand_args)) + # set all variables from args and load everything needed according to + # them + self.load_ceph_conf_path(cluster_name=args.cluster) + try: + conf.ceph = configuration.load(conf.path) + except exceptions.ConfigurationError as error: + # we warn only here, because it is possible that the configuration + # file is not needed, or that it will be loaded by some other means + # (like reading from zfs tags) + logger.exception('ignoring inability to load ceph.conf') + terminal.red(error) + # dispatch to sub-commands + terminal.dispatch(self.zfs_mapper, subcommand_args) diff --git a/src/ceph-volume/plugin/zfs/requirements_dev.txt b/src/ceph-volume/plugin/zfs/requirements_dev.txt new file mode 100644 index 000000000..7263a68fa --- /dev/null +++ b/src/ceph-volume/plugin/zfs/requirements_dev.txt @@ -0,0 +1,5 @@ +pip==9.0.1 +wheel==0.30.0 +flake8==3.5.0 +tox==2.9.1 +coverage==4.5.1 diff --git a/src/ceph-volume/plugin/zfs/setup.py b/src/ceph-volume/plugin/zfs/setup.py new file mode 100644 index 000000000..31f6998f9 --- /dev/null +++ b/src/ceph-volume/plugin/zfs/setup.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""The setup script.""" + +from setuptools import setup, find_packages + +requirements = [ ] + +setup_requirements = [ ] + +setup( + author="Willem Jan Withagen", + author_email='wjw@digiware.nl', + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Environment :: Console', + 'Intended Audience :: Information Technology', + 'Intended Audience :: System Administrators', + 'Operating System :: POSIX :: FreeBSD', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + "Programming Language :: Python :: 2", + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + ], + description="Manage Ceph OSDs on ZFS pool/volume/filesystem", + install_requires=requirements, + license="BSD license", + include_package_data=True, + keywords='ceph-volume-zfs', + name='ceph-volume-zfs', + packages=find_packages(include=['ceph_volume_zfs']), + setup_requires=setup_requirements, + url='https://github.com/ceph/ceph/src/ceph-volume/plugin/zfs', + version='0.1.0', + zip_safe=False, + entry_points = dict( + ceph_volume_handlers = [ + 'zfs = ceph_volume_zfs.zfs:ZFS', + ], + ), +) diff --git a/src/ceph-volume/plugin/zfs/tox.ini b/src/ceph-volume/plugin/zfs/tox.ini new file mode 100644 index 000000000..80e35439f --- /dev/null +++ b/src/ceph-volume/plugin/zfs/tox.ini @@ -0,0 +1,21 @@ +[tox] +envlist = py27, py34, py35, py36, flake8 + +[travis] +python = + 3.6: py36 + 3.5: py35 + 3.4: py34 + 2.7: py27 + +[testenv:flake8] +basepython = python +deps = flake8 +commands = flake8 + +[testenv] +setenv = + PYTHONPATH = {toxinidir} + +commands = python setup.py test + |