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