]>
Commit | Line | Data |
---|---|---|
3efd9988 FG |
1 | from __future__ import print_function |
2 | import argparse | |
b32b8144 | 3 | import base64 |
3efd9988 FG |
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 | |
a8e16298 | 10 | from ceph_volume.systemd import systemctl |
b32b8144 | 11 | from ceph_volume.util import arg_validators, system, disk, encryption |
91327a77 | 12 | from ceph_volume.util.device import Device |
3efd9988 FG |
13 | |
14 | ||
15 | logger = logging.getLogger(__name__) | |
16 | ||
17 | ||
b32b8144 FG |
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 | ||
3efd9988 FG |
42 | class 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) |