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