]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/devices/lvm/prepare.py
c602a705fb921a78d7ddf0494d0fcf029b324523
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / prepare.py
1 from __future__ import print_function
2 import json
3 import logging
4 from textwrap import dedent
5 from ceph_volume.util import prepare as prepare_utils
6 from ceph_volume.util import encryption as encryption_utils
7 from ceph_volume.util import system, disk
8 from ceph_volume.util.arg_validators import exclude_group_options
9 from ceph_volume import conf, decorators, terminal
10 from ceph_volume.api import lvm as api
11 from .common import prepare_parser, rollback_osd
12
13
14 logger = logging.getLogger(__name__)
15
16
17 def prepare_dmcrypt(key, device, device_type, tags):
18 """
19 Helper for devices that are encrypted. The operations needed for
20 block, db, wal, or data/journal devices are all the same
21 """
22 if not device:
23 return ''
24 tag_name = 'ceph.%s_uuid' % device_type
25 uuid = tags[tag_name]
26 # format data device
27 encryption_utils.luks_format(
28 key,
29 device
30 )
31 encryption_utils.luks_open(
32 key,
33 device,
34 uuid
35 )
36
37 return '/dev/mapper/%s' % uuid
38
39
40 def prepare_filestore(device, journal, secrets, tags, osd_id, fsid):
41 """
42 :param device: The name of the logical volume to work with
43 :param journal: similar to device but can also be a regular/plain disk
44 :param secrets: A dict with the secrets needed to create the osd (e.g. cephx)
45 :param id_: The OSD id
46 :param fsid: The OSD fsid, also known as the OSD UUID
47 """
48 cephx_secret = secrets.get('cephx_secret', prepare_utils.create_key())
49
50 # encryption-only operations
51 if secrets.get('dmcrypt_key'):
52 # format and open ('decrypt' devices) and re-assign the device and journal
53 # variables so that the rest of the process can use the mapper paths
54 key = secrets['dmcrypt_key']
55 device = prepare_dmcrypt(key, device, 'data', tags)
56 journal = prepare_dmcrypt(key, journal, 'journal', tags)
57
58 # vdo detection
59 is_vdo = api.is_vdo(device)
60 # create the directory
61 prepare_utils.create_osd_path(osd_id)
62 # format the device
63 prepare_utils.format_device(device)
64 # mount the data device
65 prepare_utils.mount_osd(device, osd_id, is_vdo=is_vdo)
66 # symlink the journal
67 prepare_utils.link_journal(journal, osd_id)
68 # get the latest monmap
69 prepare_utils.get_monmap(osd_id)
70 # prepare the osd filesystem
71 prepare_utils.osd_mkfs_filestore(osd_id, fsid, cephx_secret)
72 # write the OSD keyring if it doesn't exist already
73 prepare_utils.write_keyring(osd_id, cephx_secret)
74 if secrets.get('dmcrypt_key'):
75 # if the device is going to get activated right away, this can be done
76 # here, otherwise it will be recreated
77 encryption_utils.write_lockbox_keyring(
78 osd_id,
79 fsid,
80 tags['ceph.cephx_lockbox_secret']
81 )
82
83
84 def prepare_bluestore(block, wal, db, secrets, tags, osd_id, fsid):
85 """
86 :param block: The name of the logical volume for the bluestore data
87 :param wal: a regular/plain disk or logical volume, to be used for block.wal
88 :param db: a regular/plain disk or logical volume, to be used for block.db
89 :param secrets: A dict with the secrets needed to create the osd (e.g. cephx)
90 :param id_: The OSD id
91 :param fsid: The OSD fsid, also known as the OSD UUID
92 """
93 cephx_secret = secrets.get('cephx_secret', prepare_utils.create_key())
94 # encryption-only operations
95 if secrets.get('dmcrypt_key'):
96 # If encrypted, there is no need to create the lockbox keyring file because
97 # bluestore re-creates the files and does not have support for other files
98 # like the custom lockbox one. This will need to be done on activation.
99 # format and open ('decrypt' devices) and re-assign the device and journal
100 # variables so that the rest of the process can use the mapper paths
101 key = secrets['dmcrypt_key']
102 block = prepare_dmcrypt(key, block, 'block', tags)
103 wal = prepare_dmcrypt(key, wal, 'wal', tags)
104 db = prepare_dmcrypt(key, db, 'db', tags)
105
106 # create the directory
107 prepare_utils.create_osd_path(osd_id, tmpfs=True)
108 # symlink the block
109 prepare_utils.link_block(block, osd_id)
110 # get the latest monmap
111 prepare_utils.get_monmap(osd_id)
112 # write the OSD keyring if it doesn't exist already
113 prepare_utils.write_keyring(osd_id, cephx_secret)
114 # prepare the osd filesystem
115 prepare_utils.osd_mkfs_bluestore(
116 osd_id, fsid,
117 keyring=cephx_secret,
118 wal=wal,
119 db=db
120 )
121
122
123 class Prepare(object):
124
125 help = 'Format an LVM device and associate it with an OSD'
126
127 def __init__(self, argv):
128 self.argv = argv
129 self.osd_id = None
130
131 def get_ptuuid(self, argument):
132 uuid = disk.get_partuuid(argument)
133 if not uuid:
134 terminal.error('blkid could not detect a PARTUUID for device: %s' % argument)
135 raise RuntimeError('unable to use device')
136 return uuid
137
138 def get_lv(self, argument):
139 """
140 Perform some parsing of the command-line value so that the process
141 can determine correctly if it got a device path or an lv.
142
143 :param argument: The command-line value that will need to be split to
144 retrieve the actual lv
145 """
146 #TODO is this efficient?
147 try:
148 vg_name, lv_name = argument.split('/')
149 except (ValueError, AttributeError):
150 return None
151 return api.get_lv(lv_name=lv_name, vg_name=vg_name)
152
153 def setup_device(self, device_type, device_name, tags, size):
154 """
155 Check if ``device`` is an lv, if so, set the tags, making sure to
156 update the tags with the lv_uuid and lv_path which the incoming tags
157 will not have.
158
159 If the device is not a logical volume, then retrieve the partition UUID
160 by querying ``blkid``
161 """
162 if device_name is None:
163 return '', '', tags
164 tags['ceph.type'] = device_type
165 tags['ceph.vdo'] = api.is_vdo(device_name)
166 lv = self.get_lv(device_name)
167 if lv:
168 uuid = lv.lv_uuid
169 path = lv.lv_path
170 tags['ceph.%s_uuid' % device_type] = uuid
171 tags['ceph.%s_device' % device_type] = path
172 lv.set_tags(tags)
173 elif disk.is_device(device_name):
174 # We got a disk, create an lv
175 lv_type = "osd-{}".format(device_type)
176 uuid = system.generate_uuid()
177 tags['ceph.{}_uuid'.format(device_type)] = uuid
178 kwargs = {
179 'device': device_name,
180 'tags': tags,
181 }
182 if size != 0:
183 kwargs['size'] = disk.Size.parse(size)
184 lv = api.create_lv(
185 lv_type,
186 uuid,
187 **kwargs)
188 path = lv.lv_path
189 tags['ceph.{}_device'.format(device_type)] = path
190 lv.set_tags(tags)
191 else:
192 # otherwise assume this is a regular disk partition
193 uuid = self.get_ptuuid(device_name)
194 path = device_name
195 tags['ceph.%s_uuid' % device_type] = uuid
196 tags['ceph.%s_device' % device_type] = path
197 return path, uuid, tags
198
199 def prepare_data_device(self, device_type, osd_uuid):
200 """
201 Check if ``arg`` is a device or partition to create an LV out of it
202 with a distinct volume group name, assigning LV tags on it and
203 ultimately, returning the logical volume object. Failing to detect
204 a device or partition will result in error.
205
206 :param arg: The value of ``--data`` when parsing args
207 :param device_type: Usually, either ``data`` or ``block`` (filestore vs. bluestore)
208 :param osd_uuid: The OSD uuid
209 """
210 device = self.args.data
211 if disk.is_partition(device) or disk.is_device(device):
212 # we must create a vg, and then a single lv
213 lv_name_prefix = "osd-{}".format(device_type)
214 kwargs = {'device': device,
215 'tags': {'ceph.type': device_type},
216 }
217 logger.debug('data device size: {}'.format(self.args.data_size))
218 if self.args.data_size != 0:
219 kwargs['size'] = disk.Size.parse(self.args.data_size)
220 return api.create_lv(
221 lv_name_prefix,
222 osd_uuid,
223 **kwargs)
224 else:
225 error = [
226 'Cannot use device ({}).'.format(device),
227 'A vg/lv path or an existing device is needed']
228 raise RuntimeError(' '.join(error))
229
230 raise RuntimeError('no data logical volume found with: {}'.format(device))
231
232 def safe_prepare(self, args=None):
233 """
234 An intermediate step between `main()` and `prepare()` so that we can
235 capture the `self.osd_id` in case we need to rollback
236
237 :param args: Injected args, usually from `lvm create` which compounds
238 both `prepare` and `create`
239 """
240 if args is not None:
241 self.args = args
242 if api.is_ceph_device(self.get_lv(self.args.data)):
243 logger.info("device {} is already used".format(self.args.data))
244 raise RuntimeError("skipping {}, it is already prepared".format(self.args.data))
245 try:
246 self.prepare()
247 except Exception:
248 logger.exception('lvm prepare was unable to complete')
249 logger.info('will rollback OSD ID creation')
250 rollback_osd(self.args, self.osd_id)
251 raise
252 terminal.success("ceph-volume lvm prepare successful for: %s" % self.args.data)
253
254 def get_cluster_fsid(self):
255 """
256 Allows using --cluster-fsid as an argument, but can fallback to reading
257 from ceph.conf if that is unset (the default behavior).
258 """
259 if self.args.cluster_fsid:
260 return self.args.cluster_fsid
261 else:
262 return conf.ceph.get('global', 'fsid')
263
264 @decorators.needs_root
265 def prepare(self):
266 # FIXME we don't allow re-using a keyring, we always generate one for the
267 # OSD, this needs to be fixed. This could either be a file (!) or a string
268 # (!!) or some flags that we would need to compound into a dict so that we
269 # can convert to JSON (!!!)
270 secrets = {'cephx_secret': prepare_utils.create_key()}
271 cephx_lockbox_secret = ''
272 encrypted = 1 if self.args.dmcrypt else 0
273 cephx_lockbox_secret = '' if not encrypted else prepare_utils.create_key()
274
275 if encrypted:
276 secrets['dmcrypt_key'] = encryption_utils.create_dmcrypt_key()
277 secrets['cephx_lockbox_secret'] = cephx_lockbox_secret
278
279 cluster_fsid = self.get_cluster_fsid()
280
281 osd_fsid = self.args.osd_fsid or system.generate_uuid()
282 crush_device_class = self.args.crush_device_class
283 if crush_device_class:
284 secrets['crush_device_class'] = crush_device_class
285 # reuse a given ID if it exists, otherwise create a new ID
286 self.osd_id = prepare_utils.create_id(osd_fsid, json.dumps(secrets), osd_id=self.args.osd_id)
287 tags = {
288 'ceph.osd_fsid': osd_fsid,
289 'ceph.osd_id': self.osd_id,
290 'ceph.cluster_fsid': cluster_fsid,
291 'ceph.cluster_name': conf.cluster,
292 'ceph.crush_device_class': crush_device_class,
293 }
294 if self.args.filestore:
295 #TODO: allow auto creation of journal on passed device, only works
296 # when physical device is passed, not LV
297 if not self.args.journal:
298 raise RuntimeError('--journal is required when using --filestore')
299
300 data_lv = self.get_lv(self.args.data)
301 if not data_lv:
302 data_lv = self.prepare_data_device('data', osd_fsid)
303
304 tags['ceph.data_device'] = data_lv.lv_path
305 tags['ceph.data_uuid'] = data_lv.lv_uuid
306 tags['ceph.cephx_lockbox_secret'] = cephx_lockbox_secret
307 tags['ceph.encrypted'] = encrypted
308 tags['ceph.vdo'] = api.is_vdo(data_lv.lv_path)
309
310 journal_device, journal_uuid, tags = self.setup_device(
311 'journal', self.args.journal, tags, self.args.journal_size)
312
313 tags['ceph.type'] = 'data'
314 data_lv.set_tags(tags)
315
316 prepare_filestore(
317 data_lv.lv_path,
318 journal_device,
319 secrets,
320 tags,
321 self.osd_id,
322 osd_fsid,
323 )
324 elif self.args.bluestore:
325 block_lv = self.get_lv(self.args.data)
326 if not block_lv:
327 block_lv = self.prepare_data_device('block', osd_fsid)
328
329 tags['ceph.block_device'] = block_lv.lv_path
330 tags['ceph.block_uuid'] = block_lv.lv_uuid
331 tags['ceph.cephx_lockbox_secret'] = cephx_lockbox_secret
332 tags['ceph.encrypted'] = encrypted
333 tags['ceph.vdo'] = api.is_vdo(block_lv.lv_path)
334
335 wal_device, wal_uuid, tags = self.setup_device(
336 'wal', self.args.block_wal, tags, self.args.block_wal_size)
337 db_device, db_uuid, tags = self.setup_device(
338 'db', self.args.block_db, tags, self.args.block_db_size)
339
340 tags['ceph.type'] = 'block'
341 block_lv.set_tags(tags)
342
343 prepare_bluestore(
344 block_lv.lv_path,
345 wal_device,
346 db_device,
347 secrets,
348 tags,
349 self.osd_id,
350 osd_fsid,
351 )
352
353 def main(self):
354 sub_command_help = dedent("""
355 Prepare an OSD by assigning an ID and FSID, registering them with the
356 cluster with an ID and FSID, formatting and mounting the volume, and
357 finally by adding all the metadata to the logical volumes using LVM
358 tags, so that it can later be discovered.
359
360 Once the OSD is ready, an ad-hoc systemd unit will be enabled so that
361 it can later get activated and the OSD daemon can get started.
362
363 Encryption is supported via dmcrypt and the --dmcrypt flag.
364
365 Existing logical volume (lv):
366
367 ceph-volume lvm prepare --data {vg/lv}
368
369 Existing block device (a logical volume will be created):
370
371 ceph-volume lvm prepare --data /path/to/device
372
373 Optionally, can consume db and wal devices, partitions or logical
374 volumes. A device will get a logical volume, partitions and existing
375 logical volumes will be used as is:
376
377 ceph-volume lvm prepare --data {vg/lv} --block.wal {partition} --block.db {/path/to/device}
378 """)
379 parser = prepare_parser(
380 prog='ceph-volume lvm prepare',
381 description=sub_command_help,
382 )
383 if len(self.argv) == 0:
384 print(sub_command_help)
385 return
386 exclude_group_options(parser, argv=self.argv, groups=['filestore', 'bluestore'])
387 self.args = parser.parse_args(self.argv)
388 # the unfortunate mix of one superset for both filestore and bluestore
389 # makes this validation cumbersome
390 if self.args.filestore:
391 if not self.args.journal:
392 raise SystemExit('--journal is required when using --filestore')
393 # Default to bluestore here since defaulting it in add_argument may
394 # cause both to be True
395 if not self.args.bluestore and not self.args.filestore:
396 self.args.bluestore = True
397 self.safe_prepare()