1 from __future__
import print_function
7 from textwrap
import dedent
8 from ceph_volume
import decorators
, terminal
, conf
9 from ceph_volume
.api
import lvm
10 from ceph_volume
.systemd
import systemctl
11 from ceph_volume
.util
import arg_validators
, system
, disk
, encryption
12 from ceph_volume
.util
.device
import Device
15 logger
= logging
.getLogger(__name__
)
18 def parse_keyring(file_contents
):
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
24 [client.osd-lockbox.8d7a8ab2-5db0-4f83-a785-2809aba403d5]\n\tkey = AQDtoGha/GYJExAA7HNl7Ukhqr7AKlCpLJk6UA==\n
26 From the above case, it would return::
28 AQDtoGha/GYJExAA7HNl7Ukhqr7AKlCpLJk6UA==
30 # remove newlines that might be trailing
31 keyring
= file_contents
.strip('\n')
34 keyring
= keyring
.split(' ')[-1]
37 keyring
= keyring
.split('\n')[-1]
39 return keyring
.strip()
44 help = 'Capture metadata from all running ceph-disk OSDs, OSD data partition or directory'
46 def __init__(self
, argv
):
48 self
._etc
_path
= '/etc/ceph/osd/'
52 if os
.path
.isdir(self
._etc
_path
):
55 if not os
.path
.exists(self
._etc
_path
):
56 os
.mkdir(self
._etc
_path
)
59 error
= "OSD Configuration path (%s) needs to be a directory" % self
._etc
_path
60 raise RuntimeError(error
)
62 def get_contents(self
, path
):
63 with
open(path
, 'r') as fp
:
64 contents
= fp
.readlines()
66 return ''.join(contents
)
67 return ''.join(contents
).strip().strip('\n')
69 def scan_device(self
, path
):
70 device_metadata
= {'path': None, 'uuid': None}
72 return device_metadata
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
78 # cannot read the symlink if this is tmpfs
79 if os
.path
.islink(path
):
80 device
= os
.readlink(path
)
83 lvm_device
= lvm
.get_lv_from_argument(device
)
85 device_uuid
= lvm_device
.lv_uuid
87 device_uuid
= disk
.get_partuuid(device
)
89 device_metadata
['uuid'] = device_uuid
90 device_metadata
['path'] = device
92 return device_metadata
94 def scan_directory(self
, path
):
95 osd_metadata
= {'cluster_name': conf
.cluster
}
96 directory_files
= os
.listdir(path
)
97 if 'keyring' not in directory_files
:
99 'OSD files not found, required "keyring" file is not present at: %s' % path
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_
))
108 if os
.path
.islink(file_path
):
109 if os
.path
.exists(file_path
):
110 osd_metadata
[file_json_key
] = self
.scan_device(file_path
)
112 msg
= 'broken symlink found %s -> %s' % (file_path
, os
.path
.realpath(file_path
))
113 terminal
.warning(msg
)
116 if os
.path
.isdir(file_path
):
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
123 if system
.is_binary(file_path
):
124 logger
.info('skipping binary file: %s' % file_path
)
127 logger
.exception('skipping due to IOError on file: %s' % file_path
)
129 if os
.path
.isfile(file_path
):
130 content
= self
.get_contents(file_path
)
131 if 'keyring' in file_path
:
132 content
= parse_keyring(content
)
134 osd_metadata
[file_json_key
] = int(content
)
136 osd_metadata
[file_json_key
] = content
138 # we must scan the paths again because this might be a temporary mount
139 path_mounts
= system
.get_mounts(paths
=True)
140 device
= path_mounts
.get(path
)
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'
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)
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']
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
)
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')
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')
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
)
195 encryption
.plain_open(dmcrypt_secret
, device
, device_uuid
)
197 # If we have a directory, use that instead of checking for mounts
199 osd_metadata
= self
.scan_directory(directory
)
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
)
205 osd_metadata
= self
.scan_directory(dm_path_mount
[0])
207 with system
.tmp_mount(dm_path
, encrypted
=True) as device_path
:
208 osd_metadata
= self
.scan_directory(device_path
)
210 osd_metadata
['encrypted'] = True
211 osd_metadata
['encryption_type'] = encryption_type
212 osd_metadata
['lockbox.keyring'] = parse_keyring(lockbox_metadata
['keyring'])
215 @decorators.needs_root
216 def scan(self
, args
):
217 osd_metadata
= {'cluster_name': conf
.cluster
}
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
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')
228 mounted_osd_paths
= self
.device_mounts
.get(args
.osd_path
)
229 osd_path
= mounted_osd_paths
[0] if len(mounted_osd_paths
) else None
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
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'):
240 'Lockbox partition was not found for device: %s' % args
.osd_path
242 osd_metadata
= self
.scan_encrypted()
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
)
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
)
252 logger
.info('will scan OSD directory at path: %s', osd_path
)
253 osd_metadata
= self
.scan_directory(osd_path
)
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
)
260 if os
.path
.exists(json_path
) and not args
.stdout
:
263 '--force was not used and OSD metadata file exists: %s' % json_path
267 print(json
.dumps(osd_metadata
, indent
=4, sort_keys
=True, ensure_ascii
=False))
269 with
open(json_path
, 'w') as fp
:
270 json
.dump(osd_metadata
, fp
, indent
=4, sort_keys
=True, ensure_ascii
=False)
273 'OSD %s got scanned and metadata persisted to file: %s' % (
279 'To take over management of this scanned OSD, and disable ceph-disk and udev, run:'
281 terminal
.success(' ceph-volume simple activate %s %s' % (osd_id
, osd_fsid
))
283 if not osd_metadata
.get('data'):
284 msg
= 'Unable to determine device mounted on %s' % args
.osd_path
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
)
292 sub_command_help
= dedent("""
293 Scan running OSDs, an OSD directory (or data device) for files and configurations
294 that will allow to take over the management of the OSD.
296 Scanned OSDs will get their configurations stored in
297 /etc/ceph/osd/<id>-<fsid>.json
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::
302 ceph-volume simple scan /var/lib/ceph/osd/ceph-0
304 Which would store the metadata in a JSON file at::
306 /etc/ceph/osd/0-a9d50838-e823-43d6-b01f-2f8d0a77afc2.json
308 To scan all running OSDs:
310 ceph-volume simple scan
312 To a scan a specific running OSD:
314 ceph-volume simple scan /var/lib/ceph/osd/{cluster}-{osd id}
316 And to scan a device (mounted or unmounted) that has OSD data in it, for example /dev/sda1
318 ceph-volume simple scan /dev/sda1
320 Scanning a device or directory that belongs to an OSD not created by ceph-disk will be ingored.
322 parser
= argparse
.ArgumentParser(
323 prog
='ceph-volume simple scan',
324 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
325 description
=sub_command_help
,
331 help='If OSD has already been scanned, the JSON file will be overwritten'
337 help='Do not save to a file, output metadata to stdout'
343 type=arg_validators
.OSDPath(),
346 help='Path to an existing OSD directory or OSD data partition'
349 args
= parser
.parse_args(self
.argv
)
352 paths
.append(args
.osd_path
)
354 osd_ids
= systemctl
.get_running_osd_ids()
355 for osd_id
in osd_ids
:
356 paths
.append("/var/lib/ceph/osd/{}-{}".format(
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)
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
)
374 self
.encryption_metadata
= encryption
.legacy_encrypted(args
.osd_path
)
375 self
.is_encrypted
= self
.encryption_metadata
['encrypted']
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
)