]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/devices/lvm/batch.py
Import ceph 15.2.8
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / batch.py
CommitLineData
1adf2230 1import argparse
f91f0fd5 2from collections import namedtuple
f6b5b4d7 3import json
f91f0fd5 4import logging
1adf2230
AA
5from textwrap import dedent
6from ceph_volume import terminal, decorators
f91f0fd5
TL
7from ceph_volume.util import disk, prompt_bool, arg_validators, templates
8from ceph_volume.util import prepare
9from . import common
10from .create import Create
11from .prepare import Prepare
1adf2230 12
91327a77
AA
13mlogger = terminal.MultiLogger(__name__)
14logger = logging.getLogger(__name__)
15
1adf2230
AA
16
17device_list_template = """
18 * {path: <25} {size: <10} {state}"""
19
20
21def 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
f91f0fd5
TL
32def 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
41def 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
49def 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
80def 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
101def 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
1911f103 129 else:
f91f0fd5
TL
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
144def 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]
1adf2230
AA
148
149
150class 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
1adf2230
AA
157 Usage:
158
159 ceph-volume lvm batch [DEVICE...]
160
f91f0fd5 161 Devices can be physical block devices or LVs.
1adf2230
AA
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):
1adf2230
AA
168 parser = argparse.ArgumentParser(
169 prog='ceph-volume lvm batch',
170 formatter_class=argparse.RawDescriptionHelpFormatter,
f91f0fd5 171 description=self._help,
1adf2230
AA
172 )
173
174 parser.add_argument(
175 'devices',
176 metavar='DEVICES',
177 nargs='*',
f91f0fd5 178 type=arg_validators.ValidBatchDevice(),
1adf2230
AA
179 default=[],
180 help='Devices to provision OSDs',
181 )
11fdf7f2
TL
182 parser.add_argument(
183 '--db-devices',
184 nargs='*',
f91f0fd5 185 type=arg_validators.ValidBatchDevice(),
11fdf7f2
TL
186 default=[],
187 help='Devices to provision OSDs db volumes',
188 )
189 parser.add_argument(
190 '--wal-devices',
191 nargs='*',
f91f0fd5 192 type=arg_validators.ValidBatchDevice(),
11fdf7f2
TL
193 default=[],
194 help='Devices to provision OSDs wal volumes',
195 )
196 parser.add_argument(
197 '--journal-devices',
198 nargs='*',
f91f0fd5 199 type=arg_validators.ValidBatchDevice(),
11fdf7f2
TL
200 default=[],
201 help='Devices to provision OSDs journal volumes',
202 )
203 parser.add_argument(
f91f0fd5 204 '--auto',
11fdf7f2 205 action='store_true',
f91f0fd5
TL
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',
11fdf7f2
TL
214 help=('deploy standalone OSDs if rotational and non-rotational drives '
215 'are passed in DEVICES'),
216 )
1adf2230
AA
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',
f91f0fd5 230 help='Only report on OSD that would be created and exit',
1adf2230
AA
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',
f91f0fd5 241 choices=['json', 'json-pretty', 'pretty'],
1adf2230
AA
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 )
91327a77
AA
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(
f91f0fd5 266 '--data-slots',
91327a77 267 type=int,
f91f0fd5
TL
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,
91327a77
AA
275 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
276 )
11fdf7f2 277 parser.add_argument(
f91f0fd5 278 '--block-db-slots',
11fdf7f2 279 type=int,
f91f0fd5
TL
280 help='Provision slots on DB device, can remain unoccupied'
281 )
282 parser.add_argument(
283 '--block-wal-size',
284 type=disk.Size.parse,
11fdf7f2
TL
285 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
286 )
91327a77 287 parser.add_argument(
f91f0fd5 288 '--block-wal-slots',
91327a77 289 type=int,
f91f0fd5
TL
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,
91327a77
AA
304 help='Override the "osd_journal_size" value, in megabytes'
305 )
f91f0fd5
TL
306 parser.add_argument(
307 '--journal-slots',
308 type=int,
309 help='Provision slots on journal device, can remain unoccupied'
310 )
91327a77
AA
311 parser.add_argument(
312 '--prepare',
313 action='store_true',
314 help='Only prepare all OSDs, do not activate',
315 )
11fdf7f2
TL
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
f91f0fd5
TL
327 def report(self, plan):
328 report = self._create_report(plan)
329 print(report)
11fdf7f2 330
f91f0fd5 331 def _create_report(self, plan):
11fdf7f2 332 if self.args.format == 'pretty':
f91f0fd5
TL
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
11fdf7f2 341 else:
f91f0fd5
TL
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
11fdf7f2 378 else:
f91f0fd5 379 self.args.db_devices = ssd
11fdf7f2
TL
380
381 @decorators.needs_root
382 def main(self):
383 if not self.args.devices:
384 return self.parser.print_help()
1adf2230
AA
385
386 # Default to bluestore here since defaulting it in add_argument may
387 # cause both to be True
11fdf7f2
TL
388 if not self.args.bluestore and not self.args.filestore:
389 self.args.bluestore = True
1adf2230 390
f91f0fd5
TL
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)
11fdf7f2
TL
403
404 if self.args.report:
f91f0fd5
TL
405 self.report(plan)
406 return 0
11fdf7f2 407
f91f0fd5
TL
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)
11fdf7f2 414
f91f0fd5
TL
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()}