]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/devices/lvm/batch.py
a6f7632b3170c670839b05a611d9e25073bf8ddf
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / batch.py
1 import argparse
2 from collections import namedtuple
3 import json
4 import logging
5 from textwrap import dedent
6 from ceph_volume import terminal, decorators
7 from ceph_volume.util import disk, prompt_bool, arg_validators, templates
8 from ceph_volume.util import prepare
9 from . import common
10 from .create import Create
11 from .prepare import Prepare
12
13 mlogger = terminal.MultiLogger(__name__)
14 logger = logging.getLogger(__name__)
15
16
17 device_list_template = """
18 * {path: <25} {size: <10} {state}"""
19
20
21 def device_formatter(devices):
22 lines = []
23 for path, details in devices:
24 lines.append(device_list_template.format(
25 path=path, size=details['human_readable_size'],
26 state='solid' if details['rotational'] == '0' else 'rotational')
27 )
28
29 return ''.join(lines)
30
31
32 def ensure_disjoint_device_lists(data, db=[], wal=[], journal=[]):
33 # check that all device lists are disjoint with each other
34 if not all([set(data).isdisjoint(set(db)),
35 set(data).isdisjoint(set(wal)),
36 set(data).isdisjoint(set(journal)),
37 set(db).isdisjoint(set(wal))]):
38 raise Exception('Device lists are not disjoint')
39
40
41 def separate_devices_from_lvs(devices):
42 phys = []
43 lvm = []
44 for d in devices:
45 phys.append(d) if d.is_device else lvm.append(d)
46 return phys, lvm
47
48
49 def get_physical_osds(devices, args):
50 '''
51 Goes through passed physical devices and assigns OSDs
52 '''
53 data_slots = args.osds_per_device
54 if args.data_slots:
55 data_slots = max(args.data_slots, args.osds_per_device)
56 rel_data_size = 1.0 / data_slots
57 mlogger.debug('relative data size: {}'.format(rel_data_size))
58 ret = []
59 for dev in devices:
60 if dev.available_lvm:
61 dev_size = dev.vg_size[0]
62 abs_size = disk.Size(b=int(dev_size * rel_data_size))
63 free_size = dev.vg_free[0]
64 for _ in range(args.osds_per_device):
65 if abs_size > free_size:
66 break
67 free_size -= abs_size.b
68 osd_id = None
69 if args.osd_ids:
70 osd_id = args.osd_ids.pop()
71 ret.append(Batch.OSD(dev.path,
72 rel_data_size,
73 abs_size,
74 args.osds_per_device,
75 osd_id,
76 'dmcrypt' if args.dmcrypt else None))
77 return ret
78
79
80 def get_lvm_osds(lvs, args):
81 '''
82 Goes through passed LVs and assigns planned osds
83 '''
84 ret = []
85 for lv in lvs:
86 if lv.used_by_ceph:
87 continue
88 osd_id = None
89 if args.osd_ids:
90 osd_id = args.osd_ids.pop()
91 osd = Batch.OSD("{}/{}".format(lv.vg_name, lv.lv_name),
92 100.0,
93 disk.Size(b=int(lv.lvs[0].lv_size)),
94 1,
95 osd_id,
96 'dmcrypt' if args.dmcrypt else None)
97 ret.append(osd)
98 return ret
99
100
101 def get_physical_fast_allocs(devices, type_, fast_slots_per_device, new_osds, args):
102 requested_slots = getattr(args, '{}_slots'.format(type_))
103 if not requested_slots or requested_slots < fast_slots_per_device:
104 if requested_slots:
105 mlogger.info('{}_slots argument is to small, ignoring'.format(type_))
106 requested_slots = fast_slots_per_device
107
108 requested_size = getattr(args, '{}_size'.format(type_), 0)
109 if requested_size == 0:
110 # no size argument was specified, check ceph.conf
111 get_size_fct = getattr(prepare, 'get_{}_size'.format(type_))
112 requested_size = get_size_fct(lv_format=False)
113
114 ret = []
115 for dev in devices:
116 if not dev.available_lvm:
117 continue
118 # any LV present is considered a taken slot
119 occupied_slots = len(dev.lvs)
120 # this only looks at the first vg on device, unsure if there is a better
121 # way
122 dev_size = dev.vg_size[0]
123 abs_size = disk.Size(b=int(dev_size / requested_slots))
124 free_size = dev.vg_free[0]
125 relative_size = int(abs_size) / dev_size
126 if requested_size:
127 if requested_size <= abs_size:
128 abs_size = requested_size
129 else:
130 mlogger.error(
131 '{} was requested for {}, but only {} can be fulfilled'.format(
132 requested_size,
133 '{}_size'.format(type_),
134 abs_size,
135 ))
136 exit(1)
137 while abs_size <= free_size and len(ret) < new_osds and occupied_slots < fast_slots_per_device:
138 free_size -= abs_size.b
139 occupied_slots += 1
140 ret.append((dev.path, relative_size, abs_size, requested_slots))
141 return ret
142
143
144 def get_lvm_fast_allocs(lvs):
145 return [("{}/{}".format(d.vg_name, d.lv_name), 100.0,
146 disk.Size(b=int(d.lvs[0].lv_size)), 1) for d in lvs if not
147 d.used_by_ceph]
148
149
150 class Batch(object):
151
152 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
153
154 _help = dedent("""
155 Automatically size devices ready for OSD provisioning based on default strategies.
156
157 Usage:
158
159 ceph-volume lvm batch [DEVICE...]
160
161 Devices can be physical block devices or LVs.
162 Optional reporting on possible outcomes is enabled with --report
163
164 ceph-volume lvm batch --report [DEVICE...]
165 """)
166
167 def __init__(self, argv):
168 parser = argparse.ArgumentParser(
169 prog='ceph-volume lvm batch',
170 formatter_class=argparse.RawDescriptionHelpFormatter,
171 description=self._help,
172 )
173
174 parser.add_argument(
175 'devices',
176 metavar='DEVICES',
177 nargs='*',
178 type=arg_validators.ValidBatchDevice(),
179 default=[],
180 help='Devices to provision OSDs',
181 )
182 parser.add_argument(
183 '--db-devices',
184 nargs='*',
185 type=arg_validators.ValidBatchDevice(),
186 default=[],
187 help='Devices to provision OSDs db volumes',
188 )
189 parser.add_argument(
190 '--wal-devices',
191 nargs='*',
192 type=arg_validators.ValidBatchDevice(),
193 default=[],
194 help='Devices to provision OSDs wal volumes',
195 )
196 parser.add_argument(
197 '--journal-devices',
198 nargs='*',
199 type=arg_validators.ValidBatchDevice(),
200 default=[],
201 help='Devices to provision OSDs journal volumes',
202 )
203 parser.add_argument(
204 '--auto',
205 action='store_true',
206 help=('deploy multi-device OSDs if rotational and non-rotational drives '
207 'are passed in DEVICES'),
208 default=True
209 )
210 parser.add_argument(
211 '--no-auto',
212 action='store_false',
213 dest='auto',
214 help=('deploy standalone OSDs if rotational and non-rotational drives '
215 'are passed in DEVICES'),
216 )
217 parser.add_argument(
218 '--bluestore',
219 action='store_true',
220 help='bluestore objectstore (default)',
221 )
222 parser.add_argument(
223 '--filestore',
224 action='store_true',
225 help='filestore objectstore',
226 )
227 parser.add_argument(
228 '--report',
229 action='store_true',
230 help='Only report on OSD that would be created and exit',
231 )
232 parser.add_argument(
233 '--yes',
234 action='store_true',
235 help='Avoid prompting for confirmation when provisioning',
236 )
237 parser.add_argument(
238 '--format',
239 help='output format, defaults to "pretty"',
240 default='pretty',
241 choices=['json', 'json-pretty', 'pretty'],
242 )
243 parser.add_argument(
244 '--dmcrypt',
245 action='store_true',
246 help='Enable device encryption via dm-crypt',
247 )
248 parser.add_argument(
249 '--crush-device-class',
250 dest='crush_device_class',
251 help='Crush device class to assign this OSD to',
252 )
253 parser.add_argument(
254 '--no-systemd',
255 dest='no_systemd',
256 action='store_true',
257 help='Skip creating and enabling systemd units and starting OSD services',
258 )
259 parser.add_argument(
260 '--osds-per-device',
261 type=int,
262 default=1,
263 help='Provision more than 1 (the default) OSD per device',
264 )
265 parser.add_argument(
266 '--data-slots',
267 type=int,
268 help=('Provision more than 1 (the default) OSD slot per device'
269 ' if more slots then osds-per-device are specified, slots'
270 'will stay unoccupied'),
271 )
272 parser.add_argument(
273 '--block-db-size',
274 type=disk.Size.parse,
275 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
276 )
277 parser.add_argument(
278 '--block-db-slots',
279 type=int,
280 help='Provision slots on DB device, can remain unoccupied'
281 )
282 parser.add_argument(
283 '--block-wal-size',
284 type=disk.Size.parse,
285 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
286 )
287 parser.add_argument(
288 '--block-wal-slots',
289 type=int,
290 help='Provision slots on WAL device, can remain unoccupied'
291 )
292 def journal_size_in_mb_hack(size):
293 # TODO give user time to adjust, then remove this
294 if size and size[-1].isdigit():
295 mlogger.warning('DEPRECATION NOTICE')
296 mlogger.warning('--journal-size as integer is parsed as megabytes')
297 mlogger.warning('A future release will parse integers as bytes')
298 mlogger.warning('Add a "M" to explicitly pass a megabyte size')
299 size += 'M'
300 return disk.Size.parse(size)
301 parser.add_argument(
302 '--journal-size',
303 type=journal_size_in_mb_hack,
304 help='Override the "osd_journal_size" value, in megabytes'
305 )
306 parser.add_argument(
307 '--journal-slots',
308 type=int,
309 help='Provision slots on journal device, can remain unoccupied'
310 )
311 parser.add_argument(
312 '--prepare',
313 action='store_true',
314 help='Only prepare all OSDs, do not activate',
315 )
316 parser.add_argument(
317 '--osd-ids',
318 nargs='*',
319 default=[],
320 help='Reuse existing OSD ids',
321 )
322 self.args = parser.parse_args(argv)
323 self.parser = parser
324 for dev_list in ['', 'db_', 'wal_', 'journal_']:
325 setattr(self, '{}usable'.format(dev_list), [])
326
327 def report(self, plan):
328 report = self._create_report(plan)
329 print(report)
330
331 def _create_report(self, plan):
332 if self.args.format == 'pretty':
333 report = ''
334 report += templates.total_osds.format(total_osds=len(plan))
335
336 report += templates.osd_component_titles
337 for osd in plan:
338 report += templates.osd_header
339 report += osd.report()
340 return report
341 else:
342 json_report = []
343 for osd in plan:
344 json_report.append(osd.report_json())
345 if self.args.format == 'json':
346 return json.dumps(json_report)
347 elif self.args.format == 'json-pretty':
348 return json.dumps(json_report, indent=4,
349 sort_keys=True)
350
351 def _check_slot_args(self):
352 '''
353 checking if -slots args are consistent with other arguments
354 '''
355 if self.args.data_slots and self.args.osds_per_device:
356 if self.args.data_slots < self.args.osds_per_device:
357 raise ValueError('data_slots is smaller then osds_per_device')
358
359 def _sort_rotational_disks(self):
360 '''
361 Helper for legacy auto behaviour.
362 Sorts drives into rotating and non-rotating, the latter being used for
363 db or journal.
364 '''
365 mlogger.warning('DEPRECATION NOTICE')
366 mlogger.warning('You are using the legacy automatic disk sorting behavior')
367 mlogger.warning('The Pacific release will change the default to --no-auto')
368 rotating = []
369 ssd = []
370 for d in self.args.devices:
371 rotating.append(d) if d.rotational else ssd.append(d)
372 if ssd and not rotating:
373 # no need for additional sorting, we'll only deploy standalone on ssds
374 return
375 self.args.devices = rotating
376 if self.args.filestore:
377 self.args.journal_devices = ssd
378 else:
379 self.args.db_devices = ssd
380
381 @decorators.needs_root
382 def main(self):
383 if not self.args.devices:
384 return self.parser.print_help()
385
386 # Default to bluestore here since defaulting it in add_argument may
387 # cause both to be True
388 if not self.args.bluestore and not self.args.filestore:
389 self.args.bluestore = True
390
391 if (self.args.auto and not self.args.db_devices and not
392 self.args.wal_devices and not self.args.journal_devices):
393 self._sort_rotational_disks()
394
395 self._check_slot_args()
396
397 ensure_disjoint_device_lists(self.args.devices,
398 self.args.db_devices,
399 self.args.wal_devices,
400 self.args.journal_devices)
401
402 plan = self.get_plan(self.args)
403
404 if self.args.report:
405 self.report(plan)
406 return 0
407
408 if not self.args.yes:
409 self.report(plan)
410 terminal.info('The above OSDs would be created if the operation continues')
411 if not prompt_bool('do you want to proceed? (yes/no)'):
412 terminal.error('aborting OSD provisioning')
413 raise SystemExit(0)
414
415 self._execute(plan)
416
417 def _execute(self, plan):
418 defaults = common.get_default_args()
419 global_args = [
420 'bluestore',
421 'filestore',
422 'dmcrypt',
423 'crush_device_class',
424 'no_systemd',
425 ]
426 defaults.update({arg: getattr(self.args, arg) for arg in global_args})
427 for osd in plan:
428 args = osd.get_args(defaults)
429 if self.args.prepare:
430 p = Prepare([])
431 p.safe_prepare(argparse.Namespace(**args))
432 else:
433 c = Create([])
434 c.create(argparse.Namespace(**args))
435
436
437 def get_plan(self, args):
438 if args.bluestore:
439 plan = self.get_deployment_layout(args, args.devices, args.db_devices,
440 args.wal_devices)
441 elif args.filestore:
442 plan = self.get_deployment_layout(args, args.devices, args.journal_devices)
443 return plan
444
445 def get_deployment_layout(self, args, devices, fast_devices=[],
446 very_fast_devices=[]):
447 '''
448 The methods here are mostly just organization, error reporting and
449 setting up of (default) args. The heavy lifting code for the deployment
450 layout can be found in the static get_*_osds and get_*_fast_allocs
451 functions.
452 '''
453 plan = []
454 phys_devs, lvm_devs = separate_devices_from_lvs(devices)
455 mlogger.debug(('passed data devices: {} physical,'
456 ' {} LVM').format(len(phys_devs), len(lvm_devs)))
457
458 plan.extend(get_physical_osds(phys_devs, args))
459
460 plan.extend(get_lvm_osds(lvm_devs, args))
461
462 num_osds = len(plan)
463 if num_osds == 0:
464 mlogger.info('All data devices are unavailable')
465 return plan
466 requested_osds = args.osds_per_device * len(phys_devs) + len(lvm_devs)
467
468 fast_type = 'block_db' if args.bluestore else 'journal'
469 fast_allocations = self.fast_allocations(fast_devices,
470 requested_osds,
471 num_osds,
472 fast_type)
473 if fast_devices and not fast_allocations:
474 mlogger.info('{} fast devices were passed, but none are available'.format(len(fast_devices)))
475 return []
476 if fast_devices and not len(fast_allocations) == num_osds:
477 mlogger.error('{} fast allocations != {} num_osds'.format(
478 len(fast_allocations), num_osds))
479 exit(1)
480
481 very_fast_allocations = self.fast_allocations(very_fast_devices,
482 requested_osds,
483 num_osds,
484 'block_wal')
485 if very_fast_devices and not very_fast_allocations:
486 mlogger.info('{} very fast devices were passed, but none are available'.format(len(very_fast_devices)))
487 return []
488 if very_fast_devices and not len(very_fast_allocations) == num_osds:
489 mlogger.error('{} very fast allocations != {} num_osds'.format(
490 len(very_fast_allocations), num_osds))
491 exit(1)
492
493 for osd in plan:
494 if fast_devices:
495 osd.add_fast_device(*fast_allocations.pop(),
496 type_=fast_type)
497 if very_fast_devices and args.bluestore:
498 osd.add_very_fast_device(*very_fast_allocations.pop())
499 return plan
500
501 def fast_allocations(self, devices, requested_osds, new_osds, type_):
502 ret = []
503 if not devices:
504 return ret
505 phys_devs, lvm_devs = separate_devices_from_lvs(devices)
506 mlogger.debug(('passed {} devices: {} physical,'
507 ' {} LVM').format(type_, len(phys_devs), len(lvm_devs)))
508
509 ret.extend(get_lvm_fast_allocs(lvm_devs))
510
511 # fill up uneven distributions across fast devices: 5 osds and 2 fast
512 # devices? create 3 slots on each device rather then deploying
513 # heterogeneous osds
514 if (requested_osds - len(lvm_devs)) % len(phys_devs):
515 fast_slots_per_device = int((requested_osds - len(lvm_devs)) / len(phys_devs)) + 1
516 else:
517 fast_slots_per_device = int((requested_osds - len(lvm_devs)) / len(phys_devs))
518
519
520 ret.extend(get_physical_fast_allocs(phys_devs,
521 type_,
522 fast_slots_per_device,
523 new_osds,
524 self.args))
525 return ret
526
527 class OSD(object):
528 '''
529 This class simply stores info about to-be-deployed OSDs and provides an
530 easy way to retrieve the necessary create arguments.
531 '''
532 VolSpec = namedtuple('VolSpec',
533 ['path',
534 'rel_size',
535 'abs_size',
536 'slots',
537 'type_'])
538
539 def __init__(self,
540 data_path,
541 rel_size,
542 abs_size,
543 slots,
544 id_,
545 encryption):
546 self.id_ = id_
547 self.data = self.VolSpec(path=data_path,
548 rel_size=rel_size,
549 abs_size=abs_size,
550 slots=slots,
551 type_='data')
552 self.fast = None
553 self.very_fast = None
554 self.encryption = encryption
555
556 def add_fast_device(self, path, rel_size, abs_size, slots, type_):
557 self.fast = self.VolSpec(path=path,
558 rel_size=rel_size,
559 abs_size=abs_size,
560 slots=slots,
561 type_=type_)
562
563 def add_very_fast_device(self, path, rel_size, abs_size, slots):
564 self.very_fast = self.VolSpec(path=path,
565 rel_size=rel_size,
566 abs_size=abs_size,
567 slots=slots,
568 type_='block_wal')
569
570 def _get_osd_plan(self):
571 plan = {
572 'data': self.data.path,
573 'data_size': self.data.abs_size,
574 'encryption': self.encryption,
575 }
576 if self.fast:
577 type_ = self.fast.type_.replace('.', '_')
578 plan.update(
579 {
580 type_: self.fast.path,
581 '{}_size'.format(type_): self.fast.abs_size,
582 })
583 if self.very_fast:
584 plan.update(
585 {
586 'block_wal': self.very_fast.path,
587 'block_wal_size': self.very_fast.abs_size,
588 })
589 if self.id_:
590 plan.update({'osd_id': self.id_})
591 return plan
592
593 def get_args(self, defaults):
594 my_defaults = defaults.copy()
595 my_defaults.update(self._get_osd_plan())
596 return my_defaults
597
598 def report(self):
599 report = ''
600 if self.id_:
601 report += templates.osd_reused_id.format(
602 id_=self.id_)
603 if self.encryption:
604 report += templates.osd_encryption.format(
605 enc=self.encryption)
606 report += templates.osd_component.format(
607 _type=self.data.type_,
608 path=self.data.path,
609 size=self.data.abs_size,
610 percent=self.data.rel_size)
611 if self.fast:
612 report += templates.osd_component.format(
613 _type=self.fast.type_,
614 path=self.fast.path,
615 size=self.fast.abs_size,
616 percent=self.fast.rel_size)
617 if self.very_fast:
618 report += templates.osd_component.format(
619 _type=self.very_fast.type_,
620 path=self.very_fast.path,
621 size=self.very_fast.abs_size,
622 percent=self.very_fast.rel_size)
623 return report
624
625 def report_json(self):
626 # cast all values to string so that the report can be dumped in to
627 # json.dumps
628 return {k: str(v) for k, v in self._get_osd_plan().items()}