summaryrefslogtreecommitdiffstats
path: root/src/ceph-volume/ceph_volume/devices/simple
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/ceph-volume/ceph_volume/devices/simple/__init__.py1
-rw-r--r--src/ceph-volume/ceph_volume/devices/simple/activate.py302
-rw-r--r--src/ceph-volume/ceph_volume/devices/simple/main.py41
-rw-r--r--src/ceph-volume/ceph_volume/devices/simple/scan.py385
-rw-r--r--src/ceph-volume/ceph_volume/devices/simple/trigger.py70
5 files changed, 799 insertions, 0 deletions
diff --git a/src/ceph-volume/ceph_volume/devices/simple/__init__.py b/src/ceph-volume/ceph_volume/devices/simple/__init__.py
new file mode 100644
index 000000000..280e130ed
--- /dev/null
+++ b/src/ceph-volume/ceph_volume/devices/simple/__init__.py
@@ -0,0 +1 @@
+from .main import Simple # noqa
diff --git a/src/ceph-volume/ceph_volume/devices/simple/activate.py b/src/ceph-volume/ceph_volume/devices/simple/activate.py
new file mode 100644
index 000000000..7439141c0
--- /dev/null
+++ b/src/ceph-volume/ceph_volume/devices/simple/activate.py
@@ -0,0 +1,302 @@
+from __future__ import print_function
+import argparse
+import base64
+import glob
+import json
+import logging
+import os
+from textwrap import dedent
+from ceph_volume import process, decorators, terminal, conf
+from ceph_volume.util import system, disk
+from ceph_volume.util import encryption as encryption_utils
+from ceph_volume.util import prepare as prepare_utils
+from ceph_volume.systemd import systemctl
+
+
+logger = logging.getLogger(__name__)
+mlogger = terminal.MultiLogger(__name__)
+
+
+class Activate(object):
+
+ help = 'Enable systemd units to mount configured devices and start a Ceph OSD'
+
+ def __init__(self, argv, from_trigger=False):
+ self.argv = argv
+ self.from_trigger = from_trigger
+ self.skip_systemd = False
+
+ def validate_devices(self, json_config):
+ """
+ ``json_config`` is the loaded dictionary coming from the JSON file. It is usually mixed with
+ other non-device items, but for sakes of comparison it doesn't really matter. This method is
+ just making sure that the keys needed exist
+ """
+ devices = json_config.keys()
+ try:
+ objectstore = json_config['type']
+ except KeyError:
+ if {'data', 'journal'}.issubset(set(devices)):
+ logger.warning(
+ '"type" key not found, assuming "filestore" since journal key is present'
+ )
+ objectstore = 'filestore'
+ else:
+ logger.warning(
+ '"type" key not found, assuming "bluestore" since journal key is not present'
+ )
+ objectstore = 'bluestore'
+
+ # Go through all the device combinations that are absolutely required,
+ # raise an error describing what was expected and what was found
+ # otherwise.
+ if objectstore == 'filestore':
+ if {'data', 'journal'}.issubset(set(devices)):
+ return True
+ else:
+ found = [i for i in devices if i in ['data', 'journal']]
+ mlogger.error("Required devices (data, and journal) not present for filestore")
+ mlogger.error('filestore devices found: %s', found)
+ raise RuntimeError('Unable to activate filestore OSD due to missing devices')
+ else:
+ # This is a bit tricky, with newer bluestore we don't need data, older implementations
+ # do (e.g. with ceph-disk). ceph-volume just uses a tmpfs that doesn't require data.
+ if {'block', 'data'}.issubset(set(devices)):
+ return True
+ else:
+ bluestore_devices = ['block.db', 'block.wal', 'block', 'data']
+ found = [i for i in devices if i in bluestore_devices]
+ mlogger.error("Required devices (block and data) not present for bluestore")
+ mlogger.error('bluestore devices found: %s', found)
+ raise RuntimeError('Unable to activate bluestore OSD due to missing devices')
+
+ def get_device(self, uuid):
+ """
+ If a device is encrypted, it will decrypt/open and return the mapper
+ path, if it isn't encrypted it will just return the device found that
+ is mapped to the uuid. This will make it easier for the caller to
+ avoid if/else to check if devices need decrypting
+
+ :param uuid: The partition uuid of the device (PARTUUID)
+ """
+ device = disk.get_device_from_partuuid(uuid)
+
+ # If device is not found, it is fine to return an empty string from the
+ # helper that finds `device`. If it finds anything and it is not
+ # encrypted, just return what was found
+ if not self.is_encrypted or not device:
+ return device
+
+ if self.encryption_type == 'luks':
+ encryption_utils.luks_open(self.dmcrypt_secret, device, uuid)
+ else:
+ encryption_utils.plain_open(self.dmcrypt_secret, device, uuid)
+
+ return '/dev/mapper/%s' % uuid
+
+ def enable_systemd_units(self, osd_id, osd_fsid):
+ """
+ * disables the ceph-disk systemd units to prevent them from running when
+ a UDEV event matches Ceph rules
+ * creates the ``simple`` systemd units to handle the activation and
+ startup of the OSD with ``osd_id`` and ``osd_fsid``
+ * enables the OSD systemd unit and finally starts the OSD.
+ """
+ if not self.from_trigger and not self.skip_systemd:
+ # means it was scanned and now activated directly, so ensure that
+ # ceph-disk units are disabled, and that the `simple` systemd unit
+ # is created and enabled
+
+ # enable the ceph-volume unit for this OSD
+ systemctl.enable_volume(osd_id, osd_fsid, 'simple')
+
+ # disable any/all ceph-disk units
+ systemctl.mask_ceph_disk()
+ terminal.warning(
+ ('All ceph-disk systemd units have been disabled to '
+ 'prevent OSDs getting triggered by UDEV events')
+ )
+ else:
+ terminal.info('Skipping enabling of `simple` systemd unit')
+ terminal.info('Skipping masking of ceph-disk systemd units')
+
+ if not self.skip_systemd:
+ # enable the OSD
+ systemctl.enable_osd(osd_id)
+
+ # start the OSD
+ systemctl.start_osd(osd_id)
+ else:
+ terminal.info(
+ 'Skipping enabling and starting OSD simple systemd unit because --no-systemd was used'
+ )
+
+ @decorators.needs_root
+ def activate(self, args):
+ with open(args.json_config, 'r') as fp:
+ osd_metadata = json.load(fp)
+
+ # Make sure that required devices are configured
+ self.validate_devices(osd_metadata)
+
+ osd_id = osd_metadata.get('whoami', args.osd_id)
+ osd_fsid = osd_metadata.get('fsid', args.osd_fsid)
+ data_uuid = osd_metadata.get('data', {}).get('uuid')
+ conf.cluster = osd_metadata.get('cluster_name', 'ceph')
+ if not data_uuid:
+ raise RuntimeError(
+ 'Unable to activate OSD %s - no "uuid" key found for data' % args.osd_id
+ )
+
+ # Encryption detection, and capturing of the keys to decrypt
+ self.is_encrypted = osd_metadata.get('encrypted', False)
+ self.encryption_type = osd_metadata.get('encryption_type')
+ if self.is_encrypted:
+ lockbox_secret = osd_metadata.get('lockbox.keyring')
+ # write the keyring always so that we can unlock
+ encryption_utils.write_lockbox_keyring(osd_id, osd_fsid, lockbox_secret)
+ # Store the secret around so that the decrypt method can reuse
+ raw_dmcrypt_secret = encryption_utils.get_dmcrypt_key(osd_id, osd_fsid)
+ # Note how both these calls need b64decode. For some reason, the
+ # way ceph-disk creates these keys, it stores them in the monitor
+ # *undecoded*, requiring this decode call again. The lvm side of
+ # encryption doesn't need it, so we are assuming here that anything
+ # that `simple` scans, will come from ceph-disk and will need this
+ # extra decode call here
+ self.dmcrypt_secret = base64.b64decode(raw_dmcrypt_secret)
+
+ cluster_name = osd_metadata.get('cluster_name', 'ceph')
+ osd_dir = '/var/lib/ceph/osd/%s-%s' % (cluster_name, osd_id)
+
+ # XXX there is no support for LVM here
+ data_device = self.get_device(data_uuid)
+
+ if not data_device:
+ raise RuntimeError("osd fsid {} doesn't exist, this file will "
+ "be skipped, consider cleaning legacy "
+ "json file {}".format(osd_metadata['fsid'], args.json_config))
+
+ journal_device = self.get_device(osd_metadata.get('journal', {}).get('uuid'))
+ block_device = self.get_device(osd_metadata.get('block', {}).get('uuid'))
+ block_db_device = self.get_device(osd_metadata.get('block.db', {}).get('uuid'))
+ block_wal_device = self.get_device(osd_metadata.get('block.wal', {}).get('uuid'))
+
+ if not system.device_is_mounted(data_device, destination=osd_dir):
+ if osd_metadata.get('type') == 'filestore':
+ prepare_utils.mount_osd(data_device, osd_id)
+ else:
+ process.run(['mount', '-v', data_device, osd_dir])
+
+ device_map = {
+ 'journal': journal_device,
+ 'block': block_device,
+ 'block.db': block_db_device,
+ 'block.wal': block_wal_device
+ }
+
+ for name, device in device_map.items():
+ if not device:
+ continue
+ # always re-do the symlink regardless if it exists, so that the journal
+ # device path that may have changed can be mapped correctly every time
+ destination = os.path.join(osd_dir, name)
+ process.run(['ln', '-snf', device, destination])
+
+ # make sure that the journal has proper permissions
+ system.chown(device)
+
+ self.enable_systemd_units(osd_id, osd_fsid)
+
+ terminal.success('Successfully activated OSD %s with FSID %s' % (osd_id, osd_fsid))
+
+ def main(self):
+ sub_command_help = dedent("""
+ Activate OSDs by mounting devices previously configured to their
+ appropriate destination::
+
+ ceph-volume simple activate {ID} {FSID}
+
+ Or using a JSON file directly::
+
+ ceph-volume simple activate --file /etc/ceph/osd/{ID}-{FSID}.json
+
+ The OSD must have been "scanned" previously (see ``ceph-volume simple
+ scan``), so that all needed OSD device information and metadata exist.
+
+ A previously scanned OSD would exist like::
+
+ /etc/ceph/osd/{ID}-{FSID}.json
+
+
+ Environment variables supported:
+
+ CEPH_VOLUME_SIMPLE_JSON_DIR: Directory location for scanned OSD JSON configs
+ """)
+ parser = argparse.ArgumentParser(
+ prog='ceph-volume simple activate',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=sub_command_help,
+ )
+ parser.add_argument(
+ 'osd_id',
+ metavar='ID',
+ nargs='?',
+ help='The ID of the OSD, usually an integer, like 0'
+ )
+ parser.add_argument(
+ 'osd_fsid',
+ metavar='FSID',
+ nargs='?',
+ help='The FSID of the OSD, similar to a SHA1'
+ )
+ parser.add_argument(
+ '--all',
+ help='Activate all OSDs with a OSD JSON config',
+ action='store_true',
+ default=False,
+ )
+ parser.add_argument(
+ '--file',
+ help='The path to a JSON file, from a scanned OSD'
+ )
+ parser.add_argument(
+ '--no-systemd',
+ dest='skip_systemd',
+ action='store_true',
+ help='Skip creating and enabling systemd units and starting OSD services',
+ )
+ if len(self.argv) == 0:
+ print(sub_command_help)
+ return
+ args = parser.parse_args(self.argv)
+ if not args.file and not args.all:
+ if not args.osd_id and not args.osd_fsid:
+ terminal.error('ID and FSID are required to find the right OSD to activate')
+ terminal.error('from a scanned OSD location in /etc/ceph/osd/')
+ raise RuntimeError('Unable to activate without both ID and FSID')
+ # don't allow a CLI flag to specify the JSON dir, because that might
+ # implicitly indicate that it would be possible to activate a json file
+ # at a non-default location which would not work at boot time if the
+ # custom location is not exposed through an ENV var
+ self.skip_systemd = args.skip_systemd
+ json_dir = os.environ.get('CEPH_VOLUME_SIMPLE_JSON_DIR', '/etc/ceph/osd/')
+ if args.all:
+ if args.file or args.osd_id:
+ mlogger.warn('--all was passed, ignoring --file and ID/FSID arguments')
+ json_configs = glob.glob('{}/*.json'.format(json_dir))
+ for json_config in json_configs:
+ mlogger.info('activating OSD specified in {}'.format(json_config))
+ args.json_config = json_config
+ try:
+ self.activate(args)
+ except RuntimeError as e:
+ terminal.warning(e.message)
+ else:
+ if args.file:
+ json_config = args.file
+ else:
+ json_config = os.path.join(json_dir, '%s-%s.json' % (args.osd_id, args.osd_fsid))
+ if not os.path.exists(json_config):
+ raise RuntimeError('Expected JSON config path not found: %s' % json_config)
+ args.json_config = json_config
+ self.activate(args)
diff --git a/src/ceph-volume/ceph_volume/devices/simple/main.py b/src/ceph-volume/ceph_volume/devices/simple/main.py
new file mode 100644
index 000000000..2119963f8
--- /dev/null
+++ b/src/ceph-volume/ceph_volume/devices/simple/main.py
@@ -0,0 +1,41 @@
+import argparse
+from textwrap import dedent
+from ceph_volume import terminal
+from . import scan
+from . import activate
+from . import trigger
+
+
+class Simple(object):
+
+ help = 'Manage already deployed OSDs with ceph-volume'
+
+ _help = dedent("""
+ Take over a deployed OSD, persisting its metadata in /etc/ceph/osd/ so that it can be managed
+ with ceph-volume directly. Avoids UDEV and ceph-disk handling.
+
+ {sub_help}
+ """)
+
+ mapper = {
+ 'scan': scan.Scan,
+ 'activate': activate.Activate,
+ 'trigger': trigger.Trigger,
+ }
+
+ 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 simple',
+ 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/ceph_volume/devices/simple/scan.py b/src/ceph-volume/ceph_volume/devices/simple/scan.py
new file mode 100644
index 000000000..ff7040beb
--- /dev/null
+++ b/src/ceph-volume/ceph_volume/devices/simple/scan.py
@@ -0,0 +1,385 @@
+from __future__ import print_function
+import argparse
+import base64
+import json
+import logging
+import os
+from textwrap import dedent
+from ceph_volume import decorators, terminal, conf
+from ceph_volume.api import lvm
+from ceph_volume.systemd import systemctl
+from ceph_volume.util import arg_validators, system, disk, encryption
+from ceph_volume.util.device import Device
+
+
+logger = logging.getLogger(__name__)
+
+
+def parse_keyring(file_contents):
+ """
+ Extract the actual key from a string. Usually from a keyring file, where
+ the keyring will be in a client section. In the case of a lockbox, it is
+ something like::
+
+ [client.osd-lockbox.8d7a8ab2-5db0-4f83-a785-2809aba403d5]\n\tkey = AQDtoGha/GYJExAA7HNl7Ukhqr7AKlCpLJk6UA==\n
+
+ From the above case, it would return::
+
+ AQDtoGha/GYJExAA7HNl7Ukhqr7AKlCpLJk6UA==
+ """
+ # remove newlines that might be trailing
+ keyring = file_contents.strip('\n')
+
+ # Now split on spaces
+ keyring = keyring.split(' ')[-1]
+
+ # Split on newlines
+ keyring = keyring.split('\n')[-1]
+
+ return keyring.strip()
+
+
+class Scan(object):
+
+ help = 'Capture metadata from all running ceph-disk OSDs, OSD data partition or directory'
+
+ def __init__(self, argv):
+ self.argv = argv
+ self._etc_path = '/etc/ceph/osd/'
+
+ @property
+ def etc_path(self):
+ if os.path.isdir(self._etc_path):
+ return self._etc_path
+
+ if not os.path.exists(self._etc_path):
+ os.mkdir(self._etc_path)
+ return self._etc_path
+
+ error = "OSD Configuration path (%s) needs to be a directory" % self._etc_path
+ raise RuntimeError(error)
+
+ def get_contents(self, path):
+ with open(path, 'r') as fp:
+ contents = fp.readlines()
+ if len(contents) > 1:
+ return ''.join(contents)
+ return ''.join(contents).strip().strip('\n')
+
+ def scan_device(self, path):
+ device_metadata = {'path': None, 'uuid': None}
+ if not path:
+ return device_metadata
+ if self.is_encrypted:
+ encryption_metadata = encryption.legacy_encrypted(path)
+ device_metadata['path'] = encryption_metadata['device']
+ device_metadata['uuid'] = disk.get_partuuid(encryption_metadata['device'])
+ return device_metadata
+ # cannot read the symlink if this is tmpfs
+ if os.path.islink(path):
+ device = os.readlink(path)
+ else:
+ device = path
+ lvm_device = lvm.get_single_lv(filters={'lv_path': device})
+ if lvm_device:
+ device_uuid = lvm_device.lv_uuid
+ else:
+ device_uuid = disk.get_partuuid(device)
+
+ device_metadata['uuid'] = device_uuid
+ device_metadata['path'] = device
+
+ return device_metadata
+
+ def scan_directory(self, path):
+ osd_metadata = {'cluster_name': conf.cluster}
+ directory_files = os.listdir(path)
+ if 'keyring' not in directory_files:
+ raise RuntimeError(
+ 'OSD files not found, required "keyring" file is not present at: %s' % path
+ )
+ for file_ in os.listdir(path):
+ file_path = os.path.join(path, file_)
+ file_json_key = file_
+ if file_.endswith('_dmcrypt'):
+ file_json_key = file_.rstrip('_dmcrypt')
+ logger.info(
+ 'reading file {}, stripping _dmcrypt suffix'.format(file_)
+ )
+ if os.path.islink(file_path):
+ if os.path.exists(file_path):
+ osd_metadata[file_json_key] = self.scan_device(file_path)
+ else:
+ msg = 'broken symlink found %s -> %s' % (file_path, os.path.realpath(file_path))
+ terminal.warning(msg)
+ logger.warning(msg)
+
+ if os.path.isdir(file_path):
+ continue
+
+ # the check for binary needs to go before the file, to avoid
+ # capturing data from binary files but still be able to capture
+ # contents from actual files later
+ try:
+ if system.is_binary(file_path):
+ logger.info('skipping binary file: %s' % file_path)
+ continue
+ except IOError:
+ logger.exception('skipping due to IOError on file: %s' % file_path)
+ continue
+ if os.path.isfile(file_path):
+ content = self.get_contents(file_path)
+ if 'keyring' in file_path:
+ content = parse_keyring(content)
+ try:
+ osd_metadata[file_json_key] = int(content)
+ except ValueError:
+ osd_metadata[file_json_key] = content
+
+ # we must scan the paths again because this might be a temporary mount
+ path_mounts = system.Mounts(paths=True)
+ device = path_mounts.get_mounts().get(path)
+
+ # it is possible to have more than one device, pick the first one, and
+ # warn that it is possible that more than one device is 'data'
+ if not device:
+ terminal.error('Unable to detect device mounted for path: %s' % path)
+ raise RuntimeError('Cannot activate OSD')
+ osd_metadata['data'] = self.scan_device(device[0] if len(device) else None)
+
+ return osd_metadata
+
+ def scan_encrypted(self, directory=None):
+ device = self.encryption_metadata['device']
+ lockbox = self.encryption_metadata['lockbox']
+ encryption_type = self.encryption_metadata['type']
+ osd_metadata = {}
+ # Get the PARTUUID of the device to make sure have the right one and
+ # that maps to the data device
+ device_uuid = disk.get_partuuid(device)
+ dm_path = '/dev/mapper/%s' % device_uuid
+ # check if this partition is already mapped
+ device_status = encryption.status(device_uuid)
+
+ # capture all the information from the lockbox first, reusing the
+ # directory scan method
+ if self.device_mounts.get(lockbox):
+ lockbox_path = self.device_mounts.get(lockbox)[0]
+ lockbox_metadata = self.scan_directory(lockbox_path)
+ # ceph-disk stores the fsid as osd-uuid in the lockbox, thanks ceph-disk
+ dmcrypt_secret = encryption.get_dmcrypt_key(
+ None, # There is no ID stored in the lockbox
+ lockbox_metadata['osd-uuid'],
+ os.path.join(lockbox_path, 'keyring')
+ )
+ else:
+ with system.tmp_mount(lockbox) as lockbox_path:
+ lockbox_metadata = self.scan_directory(lockbox_path)
+ # ceph-disk stores the fsid as osd-uuid in the lockbox, thanks ceph-disk
+ dmcrypt_secret = encryption.get_dmcrypt_key(
+ None, # There is no ID stored in the lockbox
+ lockbox_metadata['osd-uuid'],
+ os.path.join(lockbox_path, 'keyring')
+ )
+
+ if not device_status:
+ # Note how both these calls need b64decode. For some reason, the
+ # way ceph-disk creates these keys, it stores them in the monitor
+ # *undecoded*, requiring this decode call again. The lvm side of
+ # encryption doesn't need it, so we are assuming here that anything
+ # that `simple` scans, will come from ceph-disk and will need this
+ # extra decode call here
+ dmcrypt_secret = base64.b64decode(dmcrypt_secret)
+ if encryption_type == 'luks':
+ encryption.luks_open(dmcrypt_secret, device, device_uuid)
+ else:
+ encryption.plain_open(dmcrypt_secret, device, device_uuid)
+
+ # If we have a directory, use that instead of checking for mounts
+ if directory:
+ osd_metadata = self.scan_directory(directory)
+ else:
+ # Now check if that mapper is mounted already, to avoid remounting and
+ # decrypting the device
+ dm_path_mount = self.device_mounts.get(dm_path)
+ if dm_path_mount:
+ osd_metadata = self.scan_directory(dm_path_mount[0])
+ else:
+ with system.tmp_mount(dm_path, encrypted=True) as device_path:
+ osd_metadata = self.scan_directory(device_path)
+
+ osd_metadata['encrypted'] = True
+ osd_metadata['encryption_type'] = encryption_type
+ osd_metadata['lockbox.keyring'] = parse_keyring(lockbox_metadata['keyring'])
+ return osd_metadata
+
+ @decorators.needs_root
+ def scan(self, args):
+ osd_metadata = {'cluster_name': conf.cluster}
+ osd_path = None
+ logger.info('detecting if argument is a device or a directory: %s', args.osd_path)
+ if os.path.isdir(args.osd_path):
+ logger.info('will scan directly, path is a directory')
+ osd_path = args.osd_path
+ else:
+ # assume this is a device, check if it is mounted and use that path
+ logger.info('path is not a directory, will check if mounted')
+ if system.device_is_mounted(args.osd_path):
+ logger.info('argument is a device, which is mounted')
+ mounted_osd_paths = self.device_mounts.get(args.osd_path)
+ osd_path = mounted_osd_paths[0] if len(mounted_osd_paths) else None
+
+ # argument is not a directory, and it is not a device that is mounted
+ # somewhere so temporarily mount it to poke inside, otherwise, scan
+ # directly
+ if not osd_path:
+ # check if we have an encrypted device first, so that we can poke at
+ # the lockbox instead
+ if self.is_encrypted:
+ if not self.encryption_metadata.get('lockbox'):
+ raise RuntimeError(
+ 'Lockbox partition was not found for device: %s' % args.osd_path
+ )
+ osd_metadata = self.scan_encrypted()
+ else:
+ logger.info('device is not mounted, will mount it temporarily to scan')
+ with system.tmp_mount(args.osd_path) as osd_path:
+ osd_metadata = self.scan_directory(osd_path)
+ else:
+ if self.is_encrypted:
+ logger.info('will scan encrypted OSD directory at path: %s', osd_path)
+ osd_metadata = self.scan_encrypted(osd_path)
+ else:
+ logger.info('will scan OSD directory at path: %s', osd_path)
+ osd_metadata = self.scan_directory(osd_path)
+
+ osd_id = osd_metadata['whoami']
+ osd_fsid = osd_metadata['fsid']
+ filename = '%s-%s.json' % (osd_id, osd_fsid)
+ json_path = os.path.join(self.etc_path, filename)
+
+ if os.path.exists(json_path) and not args.stdout:
+ if not args.force:
+ raise RuntimeError(
+ '--force was not used and OSD metadata file exists: %s' % json_path
+ )
+
+ if args.stdout:
+ print(json.dumps(osd_metadata, indent=4, sort_keys=True, ensure_ascii=False))
+ else:
+ with open(json_path, 'w') as fp:
+ json.dump(osd_metadata, fp, indent=4, sort_keys=True, ensure_ascii=False)
+ fp.write(os.linesep)
+ terminal.success(
+ 'OSD %s got scanned and metadata persisted to file: %s' % (
+ osd_id,
+ json_path
+ )
+ )
+ terminal.success(
+ 'To take over management of this scanned OSD, and disable ceph-disk and udev, run:'
+ )
+ terminal.success(' ceph-volume simple activate %s %s' % (osd_id, osd_fsid))
+
+ if not osd_metadata.get('data'):
+ msg = 'Unable to determine device mounted on %s' % args.osd_path
+ logger.warning(msg)
+ terminal.warning(msg)
+ terminal.warning('OSD will not be able to start without this information:')
+ terminal.warning(' "data": "/path/to/device",')
+ logger.warning('Unable to determine device mounted on %s' % args.osd_path)
+
+ def main(self):
+ sub_command_help = dedent("""
+ Scan running OSDs, an OSD directory (or data device) for files and configurations
+ that will allow to take over the management of the OSD.
+
+ Scanned OSDs will get their configurations stored in
+ /etc/ceph/osd/<id>-<fsid>.json
+
+ For an OSD ID of 0 with fsid of ``a9d50838-e823-43d6-b01f-2f8d0a77afc2``
+ that could mean a scan command that looks like::
+
+ ceph-volume simple scan /var/lib/ceph/osd/ceph-0
+
+ Which would store the metadata in a JSON file at::
+
+ /etc/ceph/osd/0-a9d50838-e823-43d6-b01f-2f8d0a77afc2.json
+
+ To scan all running OSDs:
+
+ ceph-volume simple scan
+
+ To a scan a specific running OSD:
+
+ ceph-volume simple scan /var/lib/ceph/osd/{cluster}-{osd id}
+
+ And to scan a device (mounted or unmounted) that has OSD data in it, for example /dev/sda1
+
+ ceph-volume simple scan /dev/sda1
+
+ Scanning a device or directory that belongs to an OSD not created by ceph-disk will be ingored.
+ """)
+ parser = argparse.ArgumentParser(
+ prog='ceph-volume simple scan',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=sub_command_help,
+ )
+
+ parser.add_argument(
+ '-f', '--force',
+ action='store_true',
+ help='If OSD has already been scanned, the JSON file will be overwritten'
+ )
+
+ parser.add_argument(
+ '--stdout',
+ action='store_true',
+ help='Do not save to a file, output metadata to stdout'
+ )
+
+ parser.add_argument(
+ 'osd_path',
+ metavar='OSD_PATH',
+ type=arg_validators.OSDPath(),
+ nargs='?',
+ default=None,
+ help='Path to an existing OSD directory or OSD data partition'
+ )
+
+ args = parser.parse_args(self.argv)
+ paths = []
+ if args.osd_path:
+ paths.append(args.osd_path)
+ else:
+ osd_ids = systemctl.get_running_osd_ids()
+ for osd_id in osd_ids:
+ paths.append("/var/lib/ceph/osd/{}-{}".format(
+ conf.cluster,
+ osd_id,
+ ))
+
+ # Capture some environment status, so that it can be reused all over
+ self.device_mounts = system.Mounts(devices=True).get_mounts()
+ self.path_mounts = system.Mounts(paths=True).get_mounts()
+
+ for path in paths:
+ args.osd_path = path
+ device = Device(args.osd_path)
+ if device.is_partition:
+ if device.ceph_disk.type != 'data':
+ label = device.ceph_disk.partlabel
+ msg = 'Device must be the ceph data partition, but PARTLABEL reported: "%s"' % label
+ raise RuntimeError(msg)
+
+ self.encryption_metadata = encryption.legacy_encrypted(args.osd_path)
+ self.is_encrypted = self.encryption_metadata['encrypted']
+
+ if self.encryption_metadata['device'] != "tmpfs":
+ device = Device(self.encryption_metadata['device'])
+ if not device.is_ceph_disk_member:
+ terminal.warning("Ignoring %s because it's not a ceph-disk created osd." % path)
+ else:
+ self.scan(args)
+ else:
+ terminal.warning("Ignoring %s because it's not a ceph-disk created osd." % path)
diff --git a/src/ceph-volume/ceph_volume/devices/simple/trigger.py b/src/ceph-volume/ceph_volume/devices/simple/trigger.py
new file mode 100644
index 000000000..c01d9ae2a
--- /dev/null
+++ b/src/ceph-volume/ceph_volume/devices/simple/trigger.py
@@ -0,0 +1,70 @@
+from __future__ import print_function
+import argparse
+from textwrap import dedent
+from ceph_volume.exceptions import SuffixParsingError
+from ceph_volume import decorators
+from .activate import Activate
+
+
+def parse_osd_id(string):
+ osd_id = string.split('-', 1)[0]
+ if not osd_id:
+ raise SuffixParsingError('OSD id', string)
+ if osd_id.isdigit():
+ return osd_id
+ raise SuffixParsingError('OSD id', string)
+
+
+def parse_osd_uuid(string):
+ osd_id = '%s-' % parse_osd_id(string)
+ # remove the id first
+ osd_uuid = string.split(osd_id, 1)[-1]
+ if not osd_uuid:
+ raise SuffixParsingError('OSD uuid', string)
+ return osd_uuid
+
+
+class Trigger(object):
+
+ help = 'systemd helper to activate an OSD'
+
+ def __init__(self, argv):
+ self.argv = argv
+
+ @decorators.needs_root
+ def main(self):
+ sub_command_help = dedent("""
+ ** DO NOT USE DIRECTLY **
+ This tool is meant to help the systemd unit that knows about OSDs.
+
+ Proxy OSD activation to ``ceph-volume simple activate`` by parsing the
+ input from systemd, detecting the UUID and ID associated with an OSD::
+
+ ceph-volume simple trigger {SYSTEMD-DATA}
+
+ The systemd "data" is expected to be in the format of::
+
+ {OSD ID}-{OSD UUID}
+
+ The devices associated with the OSD need to have been scanned previously,
+ so that all needed metadata can be used for starting the OSD process.
+ """)
+ parser = argparse.ArgumentParser(
+ prog='ceph-volume simple trigger',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=sub_command_help,
+ )
+
+ parser.add_argument(
+ 'systemd_data',
+ metavar='SYSTEMD_DATA',
+ nargs='?',
+ help='Data from a systemd unit containing ID and UUID of the OSD, like 0-asdf-lkjh'
+ )
+ if len(self.argv) == 0:
+ print(sub_command_help)
+ return
+ args = parser.parse_args(self.argv)
+ osd_id = parse_osd_id(args.systemd_data)
+ osd_uuid = parse_osd_uuid(args.systemd_data)
+ Activate([osd_id, osd_uuid], from_trigger=True).main()