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