]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/devices/simple/scan.py
import 15.2.5
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / simple / scan.py
CommitLineData
3efd9988
FG
1from __future__ import print_function
2import argparse
b32b8144 3import base64
3efd9988
FG
4import json
5import logging
6import os
7from textwrap import dedent
8from ceph_volume import decorators, terminal, conf
9from ceph_volume.api import lvm
a8e16298 10from ceph_volume.systemd import systemctl
b32b8144 11from ceph_volume.util import arg_validators, system, disk, encryption
91327a77 12from ceph_volume.util.device import Device
3efd9988
FG
13
14
15logger = logging.getLogger(__name__)
16
17
b32b8144
FG
18def parse_keyring(file_contents):
19 """
20 Extract the actual key from a string. Usually from a keyring file, where
21 the keyring will be in a client section. In the case of a lockbox, it is
22 something like::
23
24 [client.osd-lockbox.8d7a8ab2-5db0-4f83-a785-2809aba403d5]\n\tkey = AQDtoGha/GYJExAA7HNl7Ukhqr7AKlCpLJk6UA==\n
25
26 From the above case, it would return::
27
28 AQDtoGha/GYJExAA7HNl7Ukhqr7AKlCpLJk6UA==
29 """
30 # remove newlines that might be trailing
31 keyring = file_contents.strip('\n')
32
33 # Now split on spaces
34 keyring = keyring.split(' ')[-1]
35
36 # Split on newlines
37 keyring = keyring.split('\n')[-1]
38
39 return keyring.strip()
40
41
3efd9988
FG
42class Scan(object):
43
a8e16298 44 help = 'Capture metadata from all running ceph-disk OSDs, OSD data partition or directory'
3efd9988
FG
45
46 def __init__(self, argv):
47 self.argv = argv
48 self._etc_path = '/etc/ceph/osd/'
49
50 @property
51 def etc_path(self):
52 if os.path.isdir(self._etc_path):
53 return self._etc_path
54
55 if not os.path.exists(self._etc_path):
56 os.mkdir(self._etc_path)
57 return self._etc_path
58
59 error = "OSD Configuration path (%s) needs to be a directory" % self._etc_path
60 raise RuntimeError(error)
61
62 def get_contents(self, path):
63 with open(path, 'r') as fp:
64 contents = fp.readlines()
65 if len(contents) > 1:
66 return ''.join(contents)
67 return ''.join(contents).strip().strip('\n')
68
69 def scan_device(self, path):
70 device_metadata = {'path': None, 'uuid': None}
71 if not path:
72 return device_metadata
b32b8144
FG
73 if self.is_encrypted:
74 encryption_metadata = encryption.legacy_encrypted(path)
75 device_metadata['path'] = encryption_metadata['device']
76 device_metadata['uuid'] = disk.get_partuuid(encryption_metadata['device'])
77 return device_metadata
3efd9988
FG
78 # cannot read the symlink if this is tmpfs
79 if os.path.islink(path):
80 device = os.readlink(path)
81 else:
82 device = path
f6b5b4d7 83 lvm_device = lvm.get_first_lv(filters={'lv_path': device})
3efd9988
FG
84 if lvm_device:
85 device_uuid = lvm_device.lv_uuid
86 else:
87 device_uuid = disk.get_partuuid(device)
88
89 device_metadata['uuid'] = device_uuid
90 device_metadata['path'] = device
91
92 return device_metadata
93
94 def scan_directory(self, path):
95 osd_metadata = {'cluster_name': conf.cluster}
b32b8144
FG
96 directory_files = os.listdir(path)
97 if 'keyring' not in directory_files:
98 raise RuntimeError(
99 'OSD files not found, required "keyring" file is not present at: %s' % path
100 )
9f95a23c
TL
101 for file_ in os.listdir(path):
102 file_path = os.path.join(path, file_)
103 file_json_key = file_
104 if file_.endswith('_dmcrypt'):
105 file_json_key = file_.rstrip('_dmcrypt')
1911f103
TL
106 logger.info(
107 'reading file {}, stripping _dmcrypt suffix'.format(file_)
108 )
3efd9988 109 if os.path.islink(file_path):
b32b8144 110 if os.path.exists(file_path):
9f95a23c 111 osd_metadata[file_json_key] = self.scan_device(file_path)
b32b8144
FG
112 else:
113 msg = 'broken symlink found %s -> %s' % (file_path, os.path.realpath(file_path))
114 terminal.warning(msg)
115 logger.warning(msg)
116
3efd9988
FG
117 if os.path.isdir(file_path):
118 continue
b32b8144 119
3efd9988
FG
120 # the check for binary needs to go before the file, to avoid
121 # capturing data from binary files but still be able to capture
122 # contents from actual files later
b32b8144
FG
123 try:
124 if system.is_binary(file_path):
125 logger.info('skipping binary file: %s' % file_path)
126 continue
127 except IOError:
128 logger.exception('skipping due to IOError on file: %s' % file_path)
3efd9988
FG
129 continue
130 if os.path.isfile(file_path):
b32b8144
FG
131 content = self.get_contents(file_path)
132 if 'keyring' in file_path:
133 content = parse_keyring(content)
134 try:
9f95a23c 135 osd_metadata[file_json_key] = int(content)
b32b8144 136 except ValueError:
9f95a23c 137 osd_metadata[file_json_key] = content
b32b8144
FG
138
139 # we must scan the paths again because this might be a temporary mount
140 path_mounts = system.get_mounts(paths=True)
3efd9988 141 device = path_mounts.get(path)
b32b8144 142
3efd9988
FG
143 # it is possible to have more than one device, pick the first one, and
144 # warn that it is possible that more than one device is 'data'
145 if not device:
146 terminal.error('Unable to detect device mounted for path: %s' % path)
147 raise RuntimeError('Cannot activate OSD')
148 osd_metadata['data'] = self.scan_device(device[0] if len(device) else None)
149
150 return osd_metadata
151
b32b8144
FG
152 def scan_encrypted(self, directory=None):
153 device = self.encryption_metadata['device']
154 lockbox = self.encryption_metadata['lockbox']
155 encryption_type = self.encryption_metadata['type']
156 osd_metadata = {}
157 # Get the PARTUUID of the device to make sure have the right one and
158 # that maps to the data device
159 device_uuid = disk.get_partuuid(device)
160 dm_path = '/dev/mapper/%s' % device_uuid
161 # check if this partition is already mapped
162 device_status = encryption.status(device_uuid)
163
164 # capture all the information from the lockbox first, reusing the
165 # directory scan method
166 if self.device_mounts.get(lockbox):
167 lockbox_path = self.device_mounts.get(lockbox)[0]
168 lockbox_metadata = self.scan_directory(lockbox_path)
169 # ceph-disk stores the fsid as osd-uuid in the lockbox, thanks ceph-disk
170 dmcrypt_secret = encryption.get_dmcrypt_key(
171 None, # There is no ID stored in the lockbox
172 lockbox_metadata['osd-uuid'],
173 os.path.join(lockbox_path, 'keyring')
174 )
175 else:
176 with system.tmp_mount(lockbox) as lockbox_path:
177 lockbox_metadata = self.scan_directory(lockbox_path)
178 # ceph-disk stores the fsid as osd-uuid in the lockbox, thanks ceph-disk
179 dmcrypt_secret = encryption.get_dmcrypt_key(
180 None, # There is no ID stored in the lockbox
181 lockbox_metadata['osd-uuid'],
182 os.path.join(lockbox_path, 'keyring')
183 )
184
185 if not device_status:
186 # Note how both these calls need b64decode. For some reason, the
187 # way ceph-disk creates these keys, it stores them in the monitor
188 # *undecoded*, requiring this decode call again. The lvm side of
189 # encryption doesn't need it, so we are assuming here that anything
190 # that `simple` scans, will come from ceph-disk and will need this
191 # extra decode call here
192 dmcrypt_secret = base64.b64decode(dmcrypt_secret)
193 if encryption_type == 'luks':
194 encryption.luks_open(dmcrypt_secret, device, device_uuid)
195 else:
196 encryption.plain_open(dmcrypt_secret, device, device_uuid)
197
198 # If we have a directory, use that instead of checking for mounts
199 if directory:
200 osd_metadata = self.scan_directory(directory)
201 else:
202 # Now check if that mapper is mounted already, to avoid remounting and
203 # decrypting the device
204 dm_path_mount = self.device_mounts.get(dm_path)
205 if dm_path_mount:
206 osd_metadata = self.scan_directory(dm_path_mount[0])
207 else:
208 with system.tmp_mount(dm_path, encrypted=True) as device_path:
209 osd_metadata = self.scan_directory(device_path)
210
211 osd_metadata['encrypted'] = True
212 osd_metadata['encryption_type'] = encryption_type
213 osd_metadata['lockbox.keyring'] = parse_keyring(lockbox_metadata['keyring'])
214 return osd_metadata
215
3efd9988
FG
216 @decorators.needs_root
217 def scan(self, args):
218 osd_metadata = {'cluster_name': conf.cluster}
3efd9988
FG
219 osd_path = None
220 logger.info('detecting if argument is a device or a directory: %s', args.osd_path)
221 if os.path.isdir(args.osd_path):
222 logger.info('will scan directly, path is a directory')
223 osd_path = args.osd_path
224 else:
225 # assume this is a device, check if it is mounted and use that path
226 logger.info('path is not a directory, will check if mounted')
227 if system.device_is_mounted(args.osd_path):
228 logger.info('argument is a device, which is mounted')
b32b8144 229 mounted_osd_paths = self.device_mounts.get(args.osd_path)
3efd9988
FG
230 osd_path = mounted_osd_paths[0] if len(mounted_osd_paths) else None
231
232 # argument is not a directory, and it is not a device that is mounted
233 # somewhere so temporarily mount it to poke inside, otherwise, scan
234 # directly
235 if not osd_path:
b32b8144
FG
236 # check if we have an encrypted device first, so that we can poke at
237 # the lockbox instead
238 if self.is_encrypted:
239 if not self.encryption_metadata.get('lockbox'):
240 raise RuntimeError(
241 'Lockbox partition was not found for device: %s' % args.osd_path
242 )
243 osd_metadata = self.scan_encrypted()
244 else:
245 logger.info('device is not mounted, will mount it temporarily to scan')
246 with system.tmp_mount(args.osd_path) as osd_path:
247 osd_metadata = self.scan_directory(osd_path)
3efd9988 248 else:
b32b8144
FG
249 if self.is_encrypted:
250 logger.info('will scan encrypted OSD directory at path: %s', osd_path)
251 osd_metadata = self.scan_encrypted(osd_path)
252 else:
253 logger.info('will scan OSD directory at path: %s', osd_path)
254 osd_metadata = self.scan_directory(osd_path)
3efd9988
FG
255
256 osd_id = osd_metadata['whoami']
257 osd_fsid = osd_metadata['fsid']
258 filename = '%s-%s.json' % (osd_id, osd_fsid)
259 json_path = os.path.join(self.etc_path, filename)
b32b8144 260
3efd9988
FG
261 if os.path.exists(json_path) and not args.stdout:
262 if not args.force:
263 raise RuntimeError(
264 '--force was not used and OSD metadata file exists: %s' % json_path
265 )
266
267 if args.stdout:
268 print(json.dumps(osd_metadata, indent=4, sort_keys=True, ensure_ascii=False))
269 else:
270 with open(json_path, 'w') as fp:
271 json.dump(osd_metadata, fp, indent=4, sort_keys=True, ensure_ascii=False)
11fdf7f2 272 fp.write(os.linesep)
3efd9988
FG
273 terminal.success(
274 'OSD %s got scanned and metadata persisted to file: %s' % (
275 osd_id,
276 json_path
277 )
278 )
279 terminal.success(
11fdf7f2 280 'To take over management of this scanned OSD, and disable ceph-disk and udev, run:'
3efd9988
FG
281 )
282 terminal.success(' ceph-volume simple activate %s %s' % (osd_id, osd_fsid))
283
284 if not osd_metadata.get('data'):
285 msg = 'Unable to determine device mounted on %s' % args.osd_path
286 logger.warning(msg)
287 terminal.warning(msg)
288 terminal.warning('OSD will not be able to start without this information:')
289 terminal.warning(' "data": "/path/to/device",')
290 logger.warning('Unable to determine device mounted on %s' % args.osd_path)
291
292 def main(self):
293 sub_command_help = dedent("""
a8e16298 294 Scan running OSDs, an OSD directory (or data device) for files and configurations
b32b8144 295 that will allow to take over the management of the OSD.
3efd9988
FG
296
297 Scanned OSDs will get their configurations stored in
298 /etc/ceph/osd/<id>-<fsid>.json
299
300 For an OSD ID of 0 with fsid of ``a9d50838-e823-43d6-b01f-2f8d0a77afc2``
301 that could mean a scan command that looks like::
302
92f5a8d4 303 ceph-volume simple scan /var/lib/ceph/osd/ceph-0
3efd9988
FG
304
305 Which would store the metadata in a JSON file at::
306
307 /etc/ceph/osd/0-a9d50838-e823-43d6-b01f-2f8d0a77afc2.json
308
a8e16298
TL
309 To scan all running OSDs:
310
311 ceph-volume simple scan
312
313 To a scan a specific running OSD:
3efd9988
FG
314
315 ceph-volume simple scan /var/lib/ceph/osd/{cluster}-{osd id}
316
317 And to scan a device (mounted or unmounted) that has OSD data in it, for example /dev/sda1
318
319 ceph-volume simple scan /dev/sda1
a8e16298
TL
320
321 Scanning a device or directory that belongs to an OSD not created by ceph-disk will be ingored.
3efd9988
FG
322 """)
323 parser = argparse.ArgumentParser(
324 prog='ceph-volume simple scan',
325 formatter_class=argparse.RawDescriptionHelpFormatter,
326 description=sub_command_help,
327 )
328
329 parser.add_argument(
330 '-f', '--force',
331 action='store_true',
332 help='If OSD has already been scanned, the JSON file will be overwritten'
333 )
334
335 parser.add_argument(
336 '--stdout',
337 action='store_true',
338 help='Do not save to a file, output metadata to stdout'
339 )
340
341 parser.add_argument(
342 'osd_path',
343 metavar='OSD_PATH',
344 type=arg_validators.OSDPath(),
345 nargs='?',
a8e16298 346 default=None,
3efd9988
FG
347 help='Path to an existing OSD directory or OSD data partition'
348 )
349
3efd9988 350 args = parser.parse_args(self.argv)
a8e16298
TL
351 paths = []
352 if args.osd_path:
353 paths.append(args.osd_path)
354 else:
355 osd_ids = systemctl.get_running_osd_ids()
356 for osd_id in osd_ids:
357 paths.append("/var/lib/ceph/osd/{}-{}".format(
358 conf.cluster,
359 osd_id,
360 ))
b32b8144
FG
361
362 # Capture some environment status, so that it can be reused all over
363 self.device_mounts = system.get_mounts(devices=True)
364 self.path_mounts = system.get_mounts(paths=True)
b32b8144 365
a8e16298
TL
366 for path in paths:
367 args.osd_path = path
368 device = Device(args.osd_path)
369 if device.is_partition:
370 if device.ceph_disk.type != 'data':
371 label = device.ceph_disk.partlabel
372 msg = 'Device must be the ceph data partition, but PARTLABEL reported: "%s"' % label
373 raise RuntimeError(msg)
374
375 self.encryption_metadata = encryption.legacy_encrypted(args.osd_path)
376 self.is_encrypted = self.encryption_metadata['encrypted']
377
378 device = Device(self.encryption_metadata['device'])
379 if not device.is_ceph_disk_member:
380 terminal.warning("Ignoring %s because it's not a ceph-disk created osd." % path)
381 else:
382 self.scan(args)