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