]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/devices/simple/scan.py
import 15.2.0 Octopus source
[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
83 lvm_device = lvm.get_lv_from_argument(device)
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')
106 logger.info(('reading file {}, stripping _dmcrypt',
107 'suffix').format(file_))
3efd9988 108 if os.path.islink(file_path):
b32b8144 109 if os.path.exists(file_path):
9f95a23c 110 osd_metadata[file_json_key] = self.scan_device(file_path)
b32b8144
FG
111 else:
112 msg = 'broken symlink found %s -> %s' % (file_path, os.path.realpath(file_path))
113 terminal.warning(msg)
114 logger.warning(msg)
115
3efd9988
FG
116 if os.path.isdir(file_path):
117 continue
b32b8144 118
3efd9988
FG
119 # the check for binary needs to go before the file, to avoid
120 # capturing data from binary files but still be able to capture
121 # contents from actual files later
b32b8144
FG
122 try:
123 if system.is_binary(file_path):
124 logger.info('skipping binary file: %s' % file_path)
125 continue
126 except IOError:
127 logger.exception('skipping due to IOError on file: %s' % file_path)
3efd9988
FG
128 continue
129 if os.path.isfile(file_path):
b32b8144
FG
130 content = self.get_contents(file_path)
131 if 'keyring' in file_path:
132 content = parse_keyring(content)
133 try:
9f95a23c 134 osd_metadata[file_json_key] = int(content)
b32b8144 135 except ValueError:
9f95a23c 136 osd_metadata[file_json_key] = content
b32b8144
FG
137
138 # we must scan the paths again because this might be a temporary mount
139 path_mounts = system.get_mounts(paths=True)
3efd9988 140 device = path_mounts.get(path)
b32b8144 141
3efd9988
FG
142 # it is possible to have more than one device, pick the first one, and
143 # warn that it is possible that more than one device is 'data'
144 if not device:
145 terminal.error('Unable to detect device mounted for path: %s' % path)
146 raise RuntimeError('Cannot activate OSD')
147 osd_metadata['data'] = self.scan_device(device[0] if len(device) else None)
148
149 return osd_metadata
150
b32b8144
FG
151 def scan_encrypted(self, directory=None):
152 device = self.encryption_metadata['device']
153 lockbox = self.encryption_metadata['lockbox']
154 encryption_type = self.encryption_metadata['type']
155 osd_metadata = {}
156 # Get the PARTUUID of the device to make sure have the right one and
157 # that maps to the data device
158 device_uuid = disk.get_partuuid(device)
159 dm_path = '/dev/mapper/%s' % device_uuid
160 # check if this partition is already mapped
161 device_status = encryption.status(device_uuid)
162
163 # capture all the information from the lockbox first, reusing the
164 # directory scan method
165 if self.device_mounts.get(lockbox):
166 lockbox_path = self.device_mounts.get(lockbox)[0]
167 lockbox_metadata = self.scan_directory(lockbox_path)
168 # ceph-disk stores the fsid as osd-uuid in the lockbox, thanks ceph-disk
169 dmcrypt_secret = encryption.get_dmcrypt_key(
170 None, # There is no ID stored in the lockbox
171 lockbox_metadata['osd-uuid'],
172 os.path.join(lockbox_path, 'keyring')
173 )
174 else:
175 with system.tmp_mount(lockbox) as lockbox_path:
176 lockbox_metadata = self.scan_directory(lockbox_path)
177 # ceph-disk stores the fsid as osd-uuid in the lockbox, thanks ceph-disk
178 dmcrypt_secret = encryption.get_dmcrypt_key(
179 None, # There is no ID stored in the lockbox
180 lockbox_metadata['osd-uuid'],
181 os.path.join(lockbox_path, 'keyring')
182 )
183
184 if not device_status:
185 # Note how both these calls need b64decode. For some reason, the
186 # way ceph-disk creates these keys, it stores them in the monitor
187 # *undecoded*, requiring this decode call again. The lvm side of
188 # encryption doesn't need it, so we are assuming here that anything
189 # that `simple` scans, will come from ceph-disk and will need this
190 # extra decode call here
191 dmcrypt_secret = base64.b64decode(dmcrypt_secret)
192 if encryption_type == 'luks':
193 encryption.luks_open(dmcrypt_secret, device, device_uuid)
194 else:
195 encryption.plain_open(dmcrypt_secret, device, device_uuid)
196
197 # If we have a directory, use that instead of checking for mounts
198 if directory:
199 osd_metadata = self.scan_directory(directory)
200 else:
201 # Now check if that mapper is mounted already, to avoid remounting and
202 # decrypting the device
203 dm_path_mount = self.device_mounts.get(dm_path)
204 if dm_path_mount:
205 osd_metadata = self.scan_directory(dm_path_mount[0])
206 else:
207 with system.tmp_mount(dm_path, encrypted=True) as device_path:
208 osd_metadata = self.scan_directory(device_path)
209
210 osd_metadata['encrypted'] = True
211 osd_metadata['encryption_type'] = encryption_type
212 osd_metadata['lockbox.keyring'] = parse_keyring(lockbox_metadata['keyring'])
213 return osd_metadata
214
3efd9988
FG
215 @decorators.needs_root
216 def scan(self, args):
217 osd_metadata = {'cluster_name': conf.cluster}
3efd9988
FG
218 osd_path = None
219 logger.info('detecting if argument is a device or a directory: %s', args.osd_path)
220 if os.path.isdir(args.osd_path):
221 logger.info('will scan directly, path is a directory')
222 osd_path = args.osd_path
223 else:
224 # assume this is a device, check if it is mounted and use that path
225 logger.info('path is not a directory, will check if mounted')
226 if system.device_is_mounted(args.osd_path):
227 logger.info('argument is a device, which is mounted')
b32b8144 228 mounted_osd_paths = self.device_mounts.get(args.osd_path)
3efd9988
FG
229 osd_path = mounted_osd_paths[0] if len(mounted_osd_paths) else None
230
231 # argument is not a directory, and it is not a device that is mounted
232 # somewhere so temporarily mount it to poke inside, otherwise, scan
233 # directly
234 if not osd_path:
b32b8144
FG
235 # check if we have an encrypted device first, so that we can poke at
236 # the lockbox instead
237 if self.is_encrypted:
238 if not self.encryption_metadata.get('lockbox'):
239 raise RuntimeError(
240 'Lockbox partition was not found for device: %s' % args.osd_path
241 )
242 osd_metadata = self.scan_encrypted()
243 else:
244 logger.info('device is not mounted, will mount it temporarily to scan')
245 with system.tmp_mount(args.osd_path) as osd_path:
246 osd_metadata = self.scan_directory(osd_path)
3efd9988 247 else:
b32b8144
FG
248 if self.is_encrypted:
249 logger.info('will scan encrypted OSD directory at path: %s', osd_path)
250 osd_metadata = self.scan_encrypted(osd_path)
251 else:
252 logger.info('will scan OSD directory at path: %s', osd_path)
253 osd_metadata = self.scan_directory(osd_path)
3efd9988
FG
254
255 osd_id = osd_metadata['whoami']
256 osd_fsid = osd_metadata['fsid']
257 filename = '%s-%s.json' % (osd_id, osd_fsid)
258 json_path = os.path.join(self.etc_path, filename)
b32b8144 259
3efd9988
FG
260 if os.path.exists(json_path) and not args.stdout:
261 if not args.force:
262 raise RuntimeError(
263 '--force was not used and OSD metadata file exists: %s' % json_path
264 )
265
266 if args.stdout:
267 print(json.dumps(osd_metadata, indent=4, sort_keys=True, ensure_ascii=False))
268 else:
269 with open(json_path, 'w') as fp:
270 json.dump(osd_metadata, fp, indent=4, sort_keys=True, ensure_ascii=False)
11fdf7f2 271 fp.write(os.linesep)
3efd9988
FG
272 terminal.success(
273 'OSD %s got scanned and metadata persisted to file: %s' % (
274 osd_id,
275 json_path
276 )
277 )
278 terminal.success(
11fdf7f2 279 'To take over management of this scanned OSD, and disable ceph-disk and udev, run:'
3efd9988
FG
280 )
281 terminal.success(' ceph-volume simple activate %s %s' % (osd_id, osd_fsid))
282
283 if not osd_metadata.get('data'):
284 msg = 'Unable to determine device mounted on %s' % args.osd_path
285 logger.warning(msg)
286 terminal.warning(msg)
287 terminal.warning('OSD will not be able to start without this information:')
288 terminal.warning(' "data": "/path/to/device",')
289 logger.warning('Unable to determine device mounted on %s' % args.osd_path)
290
291 def main(self):
292 sub_command_help = dedent("""
a8e16298 293 Scan running OSDs, an OSD directory (or data device) for files and configurations
b32b8144 294 that will allow to take over the management of the OSD.
3efd9988
FG
295
296 Scanned OSDs will get their configurations stored in
297 /etc/ceph/osd/<id>-<fsid>.json
298
299 For an OSD ID of 0 with fsid of ``a9d50838-e823-43d6-b01f-2f8d0a77afc2``
300 that could mean a scan command that looks like::
301
92f5a8d4 302 ceph-volume simple scan /var/lib/ceph/osd/ceph-0
3efd9988
FG
303
304 Which would store the metadata in a JSON file at::
305
306 /etc/ceph/osd/0-a9d50838-e823-43d6-b01f-2f8d0a77afc2.json
307
a8e16298
TL
308 To scan all running OSDs:
309
310 ceph-volume simple scan
311
312 To a scan a specific running OSD:
3efd9988
FG
313
314 ceph-volume simple scan /var/lib/ceph/osd/{cluster}-{osd id}
315
316 And to scan a device (mounted or unmounted) that has OSD data in it, for example /dev/sda1
317
318 ceph-volume simple scan /dev/sda1
a8e16298
TL
319
320 Scanning a device or directory that belongs to an OSD not created by ceph-disk will be ingored.
3efd9988
FG
321 """)
322 parser = argparse.ArgumentParser(
323 prog='ceph-volume simple scan',
324 formatter_class=argparse.RawDescriptionHelpFormatter,
325 description=sub_command_help,
326 )
327
328 parser.add_argument(
329 '-f', '--force',
330 action='store_true',
331 help='If OSD has already been scanned, the JSON file will be overwritten'
332 )
333
334 parser.add_argument(
335 '--stdout',
336 action='store_true',
337 help='Do not save to a file, output metadata to stdout'
338 )
339
340 parser.add_argument(
341 'osd_path',
342 metavar='OSD_PATH',
343 type=arg_validators.OSDPath(),
344 nargs='?',
a8e16298 345 default=None,
3efd9988
FG
346 help='Path to an existing OSD directory or OSD data partition'
347 )
348
3efd9988 349 args = parser.parse_args(self.argv)
a8e16298
TL
350 paths = []
351 if args.osd_path:
352 paths.append(args.osd_path)
353 else:
354 osd_ids = systemctl.get_running_osd_ids()
355 for osd_id in osd_ids:
356 paths.append("/var/lib/ceph/osd/{}-{}".format(
357 conf.cluster,
358 osd_id,
359 ))
b32b8144
FG
360
361 # Capture some environment status, so that it can be reused all over
362 self.device_mounts = system.get_mounts(devices=True)
363 self.path_mounts = system.get_mounts(paths=True)
b32b8144 364
a8e16298
TL
365 for path in paths:
366 args.osd_path = path
367 device = Device(args.osd_path)
368 if device.is_partition:
369 if device.ceph_disk.type != 'data':
370 label = device.ceph_disk.partlabel
371 msg = 'Device must be the ceph data partition, but PARTLABEL reported: "%s"' % label
372 raise RuntimeError(msg)
373
374 self.encryption_metadata = encryption.legacy_encrypted(args.osd_path)
375 self.is_encrypted = self.encryption_metadata['encrypted']
376
377 device = Device(self.encryption_metadata['device'])
378 if not device.is_ceph_disk_member:
379 terminal.warning("Ignoring %s because it's not a ceph-disk created osd." % path)
380 else:
381 self.scan(args)