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
.util
import arg_validators
, system
, disk
, encryption
11 from ceph_volume
.util
.device
import Device
14 logger
= logging
.getLogger(__name__
)
17 def parse_keyring(file_contents
):
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
23 [client.osd-lockbox.8d7a8ab2-5db0-4f83-a785-2809aba403d5]\n\tkey = AQDtoGha/GYJExAA7HNl7Ukhqr7AKlCpLJk6UA==\n
25 From the above case, it would return::
27 AQDtoGha/GYJExAA7HNl7Ukhqr7AKlCpLJk6UA==
29 # remove newlines that might be trailing
30 keyring
= file_contents
.strip('\n')
33 keyring
= keyring
.split(' ')[-1]
36 keyring
= keyring
.split('\n')[-1]
38 return keyring
.strip()
43 help = 'Capture metadata from an OSD data partition or directory'
45 def __init__(self
, argv
):
47 self
._etc
_path
= '/etc/ceph/osd/'
51 if os
.path
.isdir(self
._etc
_path
):
54 if not os
.path
.exists(self
._etc
_path
):
55 os
.mkdir(self
._etc
_path
)
58 error
= "OSD Configuration path (%s) needs to be a directory" % self
._etc
_path
59 raise RuntimeError(error
)
61 def get_contents(self
, path
):
62 with
open(path
, 'r') as fp
:
63 contents
= fp
.readlines()
65 return ''.join(contents
)
66 return ''.join(contents
).strip().strip('\n')
68 def scan_device(self
, path
):
69 device_metadata
= {'path': None, 'uuid': None}
71 return device_metadata
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
)
82 lvm_device
= lvm
.get_lv_from_argument(device
)
84 device_uuid
= lvm_device
.lv_uuid
86 device_uuid
= disk
.get_partuuid(device
)
88 device_metadata
['uuid'] = device_uuid
89 device_metadata
['path'] = device
91 return device_metadata
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
:
98 'OSD files not found, required "keyring" file is not present at: %s' % path
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
)
106 msg
= 'broken symlink found %s -> %s' % (file_path
, os
.path
.realpath(file_path
))
107 terminal
.warning(msg
)
110 if os
.path
.isdir(file_path
):
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
117 if system
.is_binary(file_path
):
118 logger
.info('skipping binary file: %s' % file_path
)
121 logger
.exception('skipping due to IOError on file: %s' % file_path
)
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
)
128 osd_metadata
[_file
] = int(content
)
130 osd_metadata
[_file
] = content
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
)
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'
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)
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']
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
)
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')
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')
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
)
189 encryption
.plain_open(dmcrypt_secret
, device
, device_uuid
)
191 # If we have a directory, use that instead of checking for mounts
193 osd_metadata
= self
.scan_directory(directory
)
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
)
199 osd_metadata
= self
.scan_directory(dm_path_mount
[0])
201 with system
.tmp_mount(dm_path
, encrypted
=True) as device_path
:
202 osd_metadata
= self
.scan_directory(device_path
)
204 osd_metadata
['encrypted'] = True
205 osd_metadata
['encryption_type'] = encryption_type
206 osd_metadata
['lockbox.keyring'] = parse_keyring(lockbox_metadata
['keyring'])
209 @decorators.needs_root
210 def scan(self
, args
):
211 osd_metadata
= {'cluster_name': conf
.cluster
}
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
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
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
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'):
234 'Lockbox partition was not found for device: %s' % args
.osd_path
236 osd_metadata
= self
.scan_encrypted()
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
)
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
)
246 logger
.info('will scan OSD directory at path: %s', osd_path
)
247 osd_metadata
= self
.scan_directory(osd_path
)
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
)
254 if os
.path
.exists(json_path
) and not args
.stdout
:
257 '--force was not used and OSD metadata file exists: %s' % json_path
261 print(json
.dumps(osd_metadata
, indent
=4, sort_keys
=True, ensure_ascii
=False))
263 with
open(json_path
, 'w') as fp
:
264 json
.dump(osd_metadata
, fp
, indent
=4, sort_keys
=True, ensure_ascii
=False)
266 'OSD %s got scanned and metadata persisted to file: %s' % (
272 'To take over managment of this scanned OSD, and disable ceph-disk and udev, run:'
274 terminal
.success(' ceph-volume simple activate %s %s' % (osd_id
, osd_fsid
))
276 if not osd_metadata
.get('data'):
277 msg
= 'Unable to determine device mounted on %s' % args
.osd_path
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
)
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.
289 Scanned OSDs will get their configurations stored in
290 /etc/ceph/osd/<id>-<fsid>.json
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::
295 ceph-volume lvm scan /var/lib/ceph/osd/ceph-0
297 Which would store the metadata in a JSON file at::
299 /etc/ceph/osd/0-a9d50838-e823-43d6-b01f-2f8d0a77afc2.json
301 To a scan an existing, running, OSD:
303 ceph-volume simple scan /var/lib/ceph/osd/{cluster}-{osd id}
305 And to scan a device (mounted or unmounted) that has OSD data in it, for example /dev/sda1
307 ceph-volume simple scan /dev/sda1
309 parser
= argparse
.ArgumentParser(
310 prog
='ceph-volume simple scan',
311 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
312 description
=sub_command_help
,
318 help='If OSD has already been scanned, the JSON file will be overwritten'
324 help='Do not save to a file, output metadata to stdout'
330 type=arg_validators
.OSDPath(),
332 help='Path to an existing OSD directory or OSD data partition'
335 if len(self
.argv
) == 0:
336 print(sub_command_help
)
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
)
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']