]>
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 | |
b32b8144 | 10 | from ceph_volume.util import arg_validators, system, disk, encryption |
91327a77 | 11 | from ceph_volume.util.device import Device |
3efd9988 FG |
12 | |
13 | ||
14 | logger = logging.getLogger(__name__) | |
15 | ||
16 | ||
b32b8144 FG |
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 | ||
3efd9988 FG |
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 | |
b32b8144 FG |
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 | |
3efd9988 FG |
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} | |
b32b8144 FG |
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 | ) | |
3efd9988 FG |
100 | for _file in os.listdir(path): |
101 | file_path = os.path.join(path, _file) | |
102 | if os.path.islink(file_path): | |
b32b8144 FG |
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 | ||
3efd9988 FG |
110 | if os.path.isdir(file_path): |
111 | continue | |
b32b8144 | 112 | |
3efd9988 FG |
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 | |
b32b8144 FG |
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) | |
3efd9988 FG |
122 | continue |
123 | if os.path.isfile(file_path): | |
b32b8144 FG |
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) | |
3efd9988 | 134 | device = path_mounts.get(path) |
b32b8144 | 135 | |
3efd9988 FG |
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 | ||
b32b8144 FG |
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 | ||
3efd9988 FG |
209 | @decorators.needs_root |
210 | def scan(self, args): | |
211 | osd_metadata = {'cluster_name': conf.cluster} | |
3efd9988 FG |
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') | |
b32b8144 | 222 | mounted_osd_paths = self.device_mounts.get(args.osd_path) |
3efd9988 FG |
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: | |
b32b8144 FG |
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) | |
3efd9988 | 241 | else: |
b32b8144 FG |
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) | |
3efd9988 FG |
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) | |
b32b8144 | 253 | |
3efd9988 FG |
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(""" | |
b32b8144 FG |
286 | Scan an OSD directory (or data device) for files and configurations |
287 | that will allow to take over the management of the OSD. | |
3efd9988 FG |
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 | |
b32b8144 | 338 | |
3efd9988 | 339 | args = parser.parse_args(self.argv) |
91327a77 AA |
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) | |
b32b8144 FG |
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 | ||
3efd9988 | 353 | self.scan(args) |