]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/devices/simple/scan.py
bump version to 15.2.1-pve1
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / simple / scan.py
1 from __future__ import print_function
2 import argparse
3 import base64
4 import json
5 import logging
6 import os
7 from textwrap import dedent
8 from ceph_volume import decorators, terminal, conf
9 from ceph_volume.api import lvm
10 from ceph_volume.systemd import systemctl
11 from ceph_volume.util import arg_validators, system, disk, encryption
12 from ceph_volume.util.device import Device
13
14
15 logger = logging.getLogger(__name__)
16
17
18 def 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
42 class Scan(object):
43
44 help = 'Capture metadata from all running ceph-disk OSDs, OSD data partition or directory'
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
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
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}
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 )
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_))
108 if os.path.islink(file_path):
109 if os.path.exists(file_path):
110 osd_metadata[file_json_key] = self.scan_device(file_path)
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
116 if os.path.isdir(file_path):
117 continue
118
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
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)
128 continue
129 if os.path.isfile(file_path):
130 content = self.get_contents(file_path)
131 if 'keyring' in file_path:
132 content = parse_keyring(content)
133 try:
134 osd_metadata[file_json_key] = int(content)
135 except ValueError:
136 osd_metadata[file_json_key] = content
137
138 # we must scan the paths again because this might be a temporary mount
139 path_mounts = system.get_mounts(paths=True)
140 device = path_mounts.get(path)
141
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
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
215 @decorators.needs_root
216 def scan(self, args):
217 osd_metadata = {'cluster_name': conf.cluster}
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')
228 mounted_osd_paths = self.device_mounts.get(args.osd_path)
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:
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)
247 else:
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)
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)
259
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)
271 fp.write(os.linesep)
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(
279 'To take over management of this scanned OSD, and disable ceph-disk and udev, run:'
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("""
293 Scan running OSDs, an OSD directory (or data device) for files and configurations
294 that will allow to take over the management of the OSD.
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
302 ceph-volume simple scan /var/lib/ceph/osd/ceph-0
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
308 To scan all running OSDs:
309
310 ceph-volume simple scan
311
312 To a scan a specific running OSD:
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
319
320 Scanning a device or directory that belongs to an OSD not created by ceph-disk will be ingored.
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='?',
345 default=None,
346 help='Path to an existing OSD directory or OSD data partition'
347 )
348
349 args = parser.parse_args(self.argv)
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 ))
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)
364
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)