]>
Commit | Line | Data |
---|---|---|
b32b8144 FG |
1 | import base64 |
2 | import os | |
3 | import logging | |
4 | from ceph_volume import process, conf | |
5 | from ceph_volume.util import constants, system | |
6 | from .prepare import write_keyring | |
7 | from .disk import lsblk, device_family, get_part_entry_type | |
8 | ||
9 | logger = logging.getLogger(__name__) | |
10 | ||
11 | ||
12 | def create_dmcrypt_key(): | |
13 | """ | |
14 | Create the secret dm-crypt key used to decrypt a device. | |
15 | """ | |
16 | # get the customizable dmcrypt key size (in bits) from ceph.conf fallback | |
17 | # to the default of 1024 | |
18 | dmcrypt_key_size = conf.ceph.get_safe( | |
19 | 'osd', | |
20 | 'osd_dmcrypt_key_size', | |
21 | default=1024, | |
22 | ) | |
23 | # The size of the key is defined in bits, so we must transform that | |
24 | # value to bytes (dividing by 8) because we read in bytes, not bits | |
25 | random_string = os.urandom(dmcrypt_key_size / 8) | |
26 | key = base64.b64encode(random_string).decode('utf-8') | |
27 | return key | |
28 | ||
29 | ||
30 | def luks_format(key, device): | |
31 | """ | |
32 | Decrypt (open) an encrypted device, previously prepared with cryptsetup | |
33 | ||
34 | :param key: dmcrypt secret key, will be used for decrypting | |
35 | :param device: Absolute path to device | |
36 | """ | |
37 | command = [ | |
38 | 'cryptsetup', | |
39 | '--batch-mode', # do not prompt | |
40 | '--key-file', # misnomer, should be key | |
41 | '-', # because we indicate stdin for the key here | |
42 | 'luksFormat', | |
43 | device, | |
44 | ] | |
45 | process.call(command, stdin=key, terminal_verbose=True, show_command=True) | |
46 | ||
47 | ||
48 | def plain_open(key, device, mapping): | |
49 | """ | |
50 | Decrypt (open) an encrypted device, previously prepared with cryptsetup in plain mode | |
51 | ||
52 | .. note: ceph-disk will require an additional b64decode call for this to work | |
53 | ||
54 | :param key: dmcrypt secret key | |
55 | :param device: absolute path to device | |
56 | :param mapping: mapping name used to correlate device. Usually a UUID | |
57 | """ | |
58 | command = [ | |
59 | 'cryptsetup', | |
60 | '--key-file', | |
61 | '-', | |
62 | 'open', | |
63 | device, | |
64 | mapping, | |
65 | '--type', 'plain', | |
66 | '--key-size', '256', | |
67 | ] | |
68 | ||
69 | process.call(command, stdin=key, terminal_verbose=True, show_command=True) | |
70 | ||
71 | ||
72 | def luks_open(key, device, mapping): | |
73 | """ | |
74 | Decrypt (open) an encrypted device, previously prepared with cryptsetup | |
75 | ||
76 | .. note: ceph-disk will require an additional b64decode call for this to work | |
77 | ||
78 | :param key: dmcrypt secret key | |
79 | :param device: absolute path to device | |
80 | :param mapping: mapping name used to correlate device. Usually a UUID | |
81 | """ | |
82 | command = [ | |
83 | 'cryptsetup', | |
84 | '--key-file', | |
85 | '-', | |
86 | 'luksOpen', | |
87 | device, | |
88 | mapping, | |
89 | ] | |
90 | process.call(command, stdin=key, terminal_verbose=True, show_command=True) | |
91 | ||
92 | ||
93 | def dmcrypt_close(mapping): | |
94 | """ | |
95 | Encrypt (close) a device, previously decrypted with cryptsetup | |
96 | ||
97 | :param mapping: | |
98 | """ | |
1adf2230 AA |
99 | if not os.path.exists(mapping): |
100 | logger.debug('device mapper path does not exist %s' % mapping) | |
101 | logger.debug('will skip cryptsetup removal') | |
102 | return | |
b32b8144 FG |
103 | process.run(['cryptsetup', 'remove', mapping]) |
104 | ||
105 | ||
106 | def get_dmcrypt_key(osd_id, osd_fsid, lockbox_keyring=None): | |
107 | """ | |
108 | Retrieve the dmcrypt (secret) key stored initially on the monitor. The key | |
109 | is sent initially with JSON, and the Monitor then mangles the name to | |
110 | ``dm-crypt/osd/<fsid>/luks`` | |
111 | ||
112 | The ``lockbox.keyring`` file is required for this operation, and it is | |
113 | assumed it will exist on the path for the same OSD that is being activated. | |
114 | To support scanning, it is optionally configurable to a custom location | |
115 | (e.g. inside a lockbox partition mounted in a temporary location) | |
116 | """ | |
117 | if lockbox_keyring is None: | |
118 | lockbox_keyring = '/var/lib/ceph/osd/%s-%s/lockbox.keyring' % (conf.cluster, osd_id) | |
119 | name = 'client.osd-lockbox.%s' % osd_fsid | |
120 | config_key = 'dm-crypt/osd/%s/luks' % osd_fsid | |
121 | ||
122 | stdout, stderr, returncode = process.call( | |
123 | [ | |
124 | 'ceph', | |
125 | '--cluster', conf.cluster, | |
126 | '--name', name, | |
127 | '--keyring', lockbox_keyring, | |
128 | 'config-key', | |
129 | 'get', | |
130 | config_key | |
131 | ], | |
132 | show_command=True | |
133 | ) | |
134 | if returncode != 0: | |
135 | raise RuntimeError('Unable to retrieve dmcrypt secret') | |
136 | return ' '.join(stdout).strip() | |
137 | ||
138 | ||
139 | def write_lockbox_keyring(osd_id, osd_fsid, secret): | |
140 | """ | |
141 | Helper to write the lockbox keyring. This is needed because the bluestore OSD will | |
142 | not persist the keyring, and it can't be stored in the data device for filestore because | |
143 | at the time this is needed, the device is encrypted. | |
144 | ||
145 | For bluestore: A tmpfs filesystem is mounted, so the path can get written | |
146 | to, but the files are ephemeral, which requires this file to be created | |
147 | every time it is activated. | |
148 | For filestore: The path for the OSD would exist at this point even if no | |
149 | OSD data device is mounted, so the keyring is written to fetch the key, and | |
150 | then the data device is mounted on that directory, making the keyring | |
151 | "dissapear". | |
152 | """ | |
153 | if os.path.exists('/var/lib/ceph/osd/%s-%s/lockbox.keyring' % (conf.cluster, osd_id)): | |
154 | return | |
155 | ||
156 | name = 'client.osd-lockbox.%s' % osd_fsid | |
157 | write_keyring( | |
158 | osd_id, | |
159 | secret, | |
160 | keyring_name='lockbox.keyring', | |
161 | name=name | |
162 | ) | |
163 | ||
164 | ||
165 | def status(device): | |
166 | """ | |
167 | Capture the metadata information of a possibly encrypted device, returning | |
168 | a dictionary with all the values found (if any). | |
169 | ||
170 | An encrypted device will contain information about a device. Example | |
171 | successful output looks like:: | |
172 | ||
173 | $ cryptsetup status /dev/mapper/ed6b5a26-eafe-4cd4-87e3-422ff61e26c4 | |
174 | /dev/mapper/ed6b5a26-eafe-4cd4-87e3-422ff61e26c4 is active and is in use. | |
175 | type: LUKS1 | |
176 | cipher: aes-xts-plain64 | |
177 | keysize: 256 bits | |
178 | device: /dev/sdc2 | |
179 | offset: 4096 sectors | |
180 | size: 20740063 sectors | |
181 | mode: read/write | |
182 | ||
183 | As long as the mapper device is in 'open' state, the ``status`` call will work. | |
184 | ||
185 | :param device: Absolute path or UUID of the device mapper | |
186 | """ | |
187 | command = [ | |
188 | 'cryptsetup', | |
189 | 'status', | |
190 | device, | |
191 | ] | |
192 | out, err, code = process.call(command, show_command=True) | |
193 | metadata = {} | |
194 | if code != 0: | |
195 | logger.warning('failed to detect device mapper information') | |
196 | return metadata | |
197 | for line in out: | |
198 | # get rid of lines that might not be useful to construct the report: | |
199 | if not line.startswith(' '): | |
200 | continue | |
201 | try: | |
202 | column, value = line.split(': ') | |
203 | except ValueError: | |
204 | continue | |
205 | metadata[column.strip()] = value.strip().strip('"') | |
206 | return metadata | |
207 | ||
208 | ||
209 | def legacy_encrypted(device): | |
210 | """ | |
211 | Detect if a device was encrypted with ceph-disk or not. In the case of | |
212 | encrypted devices, include the type of encryption (LUKS, or PLAIN), and | |
213 | infer what the lockbox partition is. | |
214 | ||
215 | This function assumes that ``device`` will be a partition. | |
216 | """ | |
217 | if os.path.isdir(device): | |
218 | mounts = system.get_mounts(paths=True) | |
219 | # yes, rebind the device variable here because a directory isn't going | |
220 | # to help with parsing | |
221 | device = mounts.get(device, [None])[0] | |
222 | if not device: | |
223 | raise RuntimeError('unable to determine the device mounted at %s' % device) | |
224 | metadata = {'encrypted': False, 'type': None, 'lockbox': '', 'device': device} | |
225 | # check if the device is online/decrypted first | |
226 | active_mapper = status(device) | |
227 | if active_mapper: | |
228 | # normalize a bit to ensure same values regardless of source | |
229 | metadata['type'] = active_mapper['type'].lower().strip('12') # turn LUKS1 or LUKS2 into luks | |
230 | metadata['encrypted'] = True if metadata['type'] in ['plain', 'luks'] else False | |
231 | # The true device is now available to this function, so it gets | |
232 | # re-assigned here for the lockbox checks to succeed (it is not | |
233 | # possible to guess partitions from a device mapper device otherwise | |
234 | device = active_mapper.get('device', device) | |
235 | metadata['device'] = device | |
236 | else: | |
237 | uuid = get_part_entry_type(device) | |
238 | guid_match = constants.ceph_disk_guids.get(uuid, {}) | |
239 | encrypted_guid = guid_match.get('encrypted', False) | |
240 | if encrypted_guid: | |
241 | metadata['encrypted'] = True | |
242 | metadata['type'] = guid_match['encryption_type'] | |
243 | ||
244 | # Lets find the lockbox location now, to do this, we need to find out the | |
245 | # parent device name for the device so that we can query all of its | |
246 | # associated devices and *then* look for one that has the 'lockbox' label | |
247 | # on it. Thanks for being awesome ceph-disk | |
248 | disk_meta = lsblk(device, abspath=True) | |
249 | if not disk_meta: | |
250 | return metadata | |
251 | parent_device = disk_meta['PKNAME'] | |
252 | # With the parent device set, we can now look for the lockbox listing associated devices | |
253 | devices = device_family(parent_device) | |
254 | for i in devices: | |
255 | if 'lockbox' in i.get('PARTLABEL', ''): | |
256 | metadata['lockbox'] = i['NAME'] | |
257 | break | |
258 | return metadata |