2 from collections
import namedtuple
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
10 from .create
import Create
11 from .prepare
import Prepare
13 mlogger
= terminal
.MultiLogger(__name__
)
14 logger
= logging
.getLogger(__name__
)
17 device_list_template
= """
18 * {path: <25} {size: <10} {state}"""
21 def device_formatter(devices
):
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')
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')
41 def separate_devices_from_lvs(devices
):
45 phys
.append(d
) if d
.is_device
else lvm
.append(d
)
49 def get_physical_osds(devices
, args
):
51 Goes through passed physical devices and assigns OSDs
53 data_slots
= args
.osds_per_device
55 data_slots
= max(args
.data_slots
, args
.osds_per_device
)
56 rel_data_size
= args
.data_allocate_fraction
/ data_slots
57 mlogger
.debug('relative data size: {}'.format(rel_data_size
))
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
:
67 free_size
-= abs_size
.b
70 osd_id
= args
.osd_ids
.pop()
71 ret
.append(Batch
.OSD(dev
.path
,
76 'dmcrypt' if args
.dmcrypt
else None))
80 def get_lvm_osds(lvs
, args
):
82 Goes through passed LVs and assigns planned osds
90 osd_id
= args
.osd_ids
.pop()
91 osd
= Batch
.OSD("{}/{}".format(lv
.vg_name
, lv
.lv_name
),
93 disk
.Size(b
=int(lv
.lvs
[0].lv_size
)),
96 'dmcrypt' if args
.dmcrypt
else None)
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
:
105 mlogger
.info('{}_slots argument is too small, ignoring'.format(type_
))
106 requested_slots
= fast_slots_per_device
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)
115 vg_device_map
= group_devices_by_vg(devices
)
116 for vg_devices
in vg_device_map
.values():
117 for dev
in vg_devices
:
118 if not dev
.available_lvm
:
120 # any LV present is considered a taken slot
121 occupied_slots
= len(dev
.lvs
)
122 # prior to v15.2.8, db/wal deployments were grouping multiple fast devices into single VGs - we need to
123 # multiply requested_slots (per device) by the number of devices in the VG in order to ensure that
124 # abs_size is calculated correctly from vg_size
125 slots_for_vg
= len(vg_devices
) * requested_slots
126 dev_size
= dev
.vg_size
[0]
127 # this only looks at the first vg on device, unsure if there is a better
129 abs_size
= disk
.Size(b
=int(dev_size
/ slots_for_vg
))
130 free_size
= dev
.vg_free
[0]
131 relative_size
= int(abs_size
) / dev_size
133 if requested_size
<= abs_size
:
134 abs_size
= requested_size
135 relative_size
= int(abs_size
) / dev_size
138 '{} was requested for {}, but only {} can be fulfilled'.format(
140 '{}_size'.format(type_
),
144 while abs_size
<= free_size
and len(ret
) < new_osds
and occupied_slots
< fast_slots_per_device
:
145 free_size
-= abs_size
.b
147 ret
.append((dev
.path
, relative_size
, abs_size
, requested_slots
))
150 def group_devices_by_vg(devices
):
152 result
['unused_devices'] = []
155 # already using assumption that a PV only belongs to single VG in other places
156 vg_name
= dev
.vgs
[0].name
157 if vg_name
in result
:
158 result
[vg_name
].append(dev
)
160 result
[vg_name
] = [dev
]
162 result
['unused_devices'].append(dev
)
165 def get_lvm_fast_allocs(lvs
):
166 return [("{}/{}".format(d
.vg_name
, d
.lv_name
), 100.0,
167 disk
.Size(b
=int(d
.lvs
[0].lv_size
)), 1) for d
in lvs
if not
173 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
176 Automatically size devices ready for OSD provisioning based on default strategies.
180 ceph-volume lvm batch [DEVICE...]
182 Devices can be physical block devices or LVs.
183 Optional reporting on possible outcomes is enabled with --report
185 ceph-volume lvm batch --report [DEVICE...]
188 def __init__(self
, argv
):
189 parser
= argparse
.ArgumentParser(
190 prog
='ceph-volume lvm batch',
191 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
192 description
=self
._help
,
199 type=arg_validators
.ValidBatchDataDevice(),
201 help='Devices to provision OSDs',
206 type=arg_validators
.ValidBatchDevice(),
208 help='Devices to provision OSDs db volumes',
213 type=arg_validators
.ValidBatchDevice(),
215 help='Devices to provision OSDs wal volumes',
220 type=arg_validators
.ValidBatchDevice(),
222 help='Devices to provision OSDs journal volumes',
227 help=('deploy multi-device OSDs if rotational and non-rotational drives '
228 'are passed in DEVICES'),
233 action
='store_false',
235 help=('deploy standalone OSDs if rotational and non-rotational drives '
236 'are passed in DEVICES'),
241 help='bluestore objectstore (default)',
246 help='filestore objectstore',
251 help='Only report on OSD that would be created and exit',
256 help='Avoid prompting for confirmation when provisioning',
260 help='output format, defaults to "pretty"',
262 choices
=['json', 'json-pretty', 'pretty'],
267 help='Enable device encryption via dm-crypt',
270 '--crush-device-class',
271 dest
='crush_device_class',
272 help='Crush device class to assign this OSD to',
279 help='Skip creating and enabling systemd units and starting OSD services',
285 help='Provision more than 1 (the default) OSD per device',
290 help=('Provision more than 1 (the default) OSD slot per device'
291 ' if more slots then osds-per-device are specified, slots'
292 'will stay unoccupied'),
295 '--data-allocate-fraction',
296 type=arg_validators
.ValidFraction(),
297 help='Fraction to allocate from data device (0,1.0]',
302 type=disk
.Size
.parse
,
303 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
308 help='Provision slots on DB device, can remain unoccupied'
312 type=disk
.Size
.parse
,
313 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
318 help='Provision slots on WAL device, can remain unoccupied'
320 def journal_size_in_mb_hack(size
):
321 # TODO give user time to adjust, then remove this
322 if size
and size
[-1].isdigit():
323 mlogger
.warning('DEPRECATION NOTICE')
324 mlogger
.warning('--journal-size as integer is parsed as megabytes')
325 mlogger
.warning('A future release will parse integers as bytes')
326 mlogger
.warning('Add a "M" to explicitly pass a megabyte size')
328 return disk
.Size
.parse(size
)
331 type=journal_size_in_mb_hack
,
332 help='Override the "osd_journal_size" value, in megabytes'
337 help='Provision slots on journal device, can remain unoccupied'
342 help='Only prepare all OSDs, do not activate',
348 help='Reuse existing OSD ids',
349 type=arg_validators
.valid_osd_id
351 self
.args
= parser
.parse_args(argv
)
353 for dev_list
in ['', 'db_', 'wal_', 'journal_']:
354 setattr(self
, '{}usable'.format(dev_list
), [])
356 def report(self
, plan
):
357 report
= self
._create
_report
(plan
)
360 def _create_report(self
, plan
):
361 if self
.args
.format
== 'pretty':
363 report
+= templates
.total_osds
.format(total_osds
=len(plan
))
365 report
+= templates
.osd_component_titles
367 report
+= templates
.osd_header
368 report
+= osd
.report()
373 json_report
.append(osd
.report_json())
374 if self
.args
.format
== 'json':
375 return json
.dumps(json_report
)
376 elif self
.args
.format
== 'json-pretty':
377 return json
.dumps(json_report
, indent
=4,
380 def _check_slot_args(self
):
382 checking if -slots args are consistent with other arguments
384 if self
.args
.data_slots
and self
.args
.osds_per_device
:
385 if self
.args
.data_slots
< self
.args
.osds_per_device
:
386 raise ValueError('data_slots is smaller then osds_per_device')
388 def _sort_rotational_disks(self
):
390 Helper for legacy auto behaviour.
391 Sorts drives into rotating and non-rotating, the latter being used for
394 mlogger
.warning('DEPRECATION NOTICE')
395 mlogger
.warning('You are using the legacy automatic disk sorting behavior')
396 mlogger
.warning('The Pacific release will change the default to --no-auto')
399 for d
in self
.args
.devices
:
400 rotating
.append(d
) if d
.rotational
else ssd
.append(d
)
401 if ssd
and not rotating
:
402 # no need for additional sorting, we'll only deploy standalone on ssds
404 self
.args
.devices
= rotating
405 if self
.args
.filestore
:
406 self
.args
.journal_devices
= ssd
408 self
.args
.db_devices
= ssd
410 @decorators.needs_root
412 if not self
.args
.devices
:
413 return self
.parser
.print_help()
415 # Default to bluestore here since defaulting it in add_argument may
416 # cause both to be True
417 if not self
.args
.bluestore
and not self
.args
.filestore
:
418 self
.args
.bluestore
= True
420 if (self
.args
.auto
and not self
.args
.db_devices
and not
421 self
.args
.wal_devices
and not self
.args
.journal_devices
):
422 self
._sort
_rotational
_disks
()
424 self
._check
_slot
_args
()
426 ensure_disjoint_device_lists(self
.args
.devices
,
427 self
.args
.db_devices
,
428 self
.args
.wal_devices
,
429 self
.args
.journal_devices
)
431 plan
= self
.get_plan(self
.args
)
437 if not self
.args
.yes
:
439 terminal
.info('The above OSDs would be created if the operation continues')
440 if not prompt_bool('do you want to proceed? (yes/no)'):
441 terminal
.error('aborting OSD provisioning')
446 def _execute(self
, plan
):
447 defaults
= common
.get_default_args()
452 'crush_device_class',
455 defaults
.update({arg
: getattr(self
.args
, arg
) for arg
in global_args
})
457 args
= osd
.get_args(defaults
)
458 if self
.args
.prepare
:
460 p
.safe_prepare(argparse
.Namespace(**args
))
463 c
.create(argparse
.Namespace(**args
))
466 def get_plan(self
, args
):
468 plan
= self
.get_deployment_layout(args
, args
.devices
, args
.db_devices
,
471 plan
= self
.get_deployment_layout(args
, args
.devices
, args
.journal_devices
)
474 def get_deployment_layout(self
, args
, devices
, fast_devices
=[],
475 very_fast_devices
=[]):
477 The methods here are mostly just organization, error reporting and
478 setting up of (default) args. The heavy lifting code for the deployment
479 layout can be found in the static get_*_osds and get_*_fast_allocs
483 phys_devs
, lvm_devs
= separate_devices_from_lvs(devices
)
484 mlogger
.debug(('passed data devices: {} physical,'
485 ' {} LVM').format(len(phys_devs
), len(lvm_devs
)))
487 plan
.extend(get_physical_osds(phys_devs
, args
))
489 plan
.extend(get_lvm_osds(lvm_devs
, args
))
493 mlogger
.info('All data devices are unavailable')
495 requested_osds
= args
.osds_per_device
* len(phys_devs
) + len(lvm_devs
)
497 fast_type
= 'block_db' if args
.bluestore
else 'journal'
498 fast_allocations
= self
.fast_allocations(fast_devices
,
502 if fast_devices
and not fast_allocations
:
503 mlogger
.info('{} fast devices were passed, but none are available'.format(len(fast_devices
)))
505 if fast_devices
and not len(fast_allocations
) == num_osds
:
506 mlogger
.error('{} fast allocations != {} num_osds'.format(
507 len(fast_allocations
), num_osds
))
510 very_fast_allocations
= self
.fast_allocations(very_fast_devices
,
514 if very_fast_devices
and not very_fast_allocations
:
515 mlogger
.info('{} very fast devices were passed, but none are available'.format(len(very_fast_devices
)))
517 if very_fast_devices
and not len(very_fast_allocations
) == num_osds
:
518 mlogger
.error('{} very fast allocations != {} num_osds'.format(
519 len(very_fast_allocations
), num_osds
))
524 osd
.add_fast_device(*fast_allocations
.pop(),
526 if very_fast_devices
and args
.bluestore
:
527 osd
.add_very_fast_device(*very_fast_allocations
.pop())
530 def fast_allocations(self
, devices
, requested_osds
, new_osds
, type_
):
534 phys_devs
, lvm_devs
= separate_devices_from_lvs(devices
)
535 mlogger
.debug(('passed {} devices: {} physical,'
536 ' {} LVM').format(type_
, len(phys_devs
), len(lvm_devs
)))
538 ret
.extend(get_lvm_fast_allocs(lvm_devs
))
540 # fill up uneven distributions across fast devices: 5 osds and 2 fast
541 # devices? create 3 slots on each device rather then deploying
543 slot_divider
= max(1, len(phys_devs
))
544 if (requested_osds
- len(lvm_devs
)) % slot_divider
:
545 fast_slots_per_device
= int((requested_osds
- len(lvm_devs
)) / slot_divider
) + 1
547 fast_slots_per_device
= int((requested_osds
- len(lvm_devs
)) / slot_divider
)
550 ret
.extend(get_physical_fast_allocs(phys_devs
,
552 fast_slots_per_device
,
559 This class simply stores info about to-be-deployed OSDs and provides an
560 easy way to retrieve the necessary create arguments.
562 VolSpec
= namedtuple('VolSpec',
577 self
.data
= self
.VolSpec(path
=data_path
,
583 self
.very_fast
= None
584 self
.encryption
= encryption
586 def add_fast_device(self
, path
, rel_size
, abs_size
, slots
, type_
):
587 self
.fast
= self
.VolSpec(path
=path
,
593 def add_very_fast_device(self
, path
, rel_size
, abs_size
, slots
):
594 self
.very_fast
= self
.VolSpec(path
=path
,
600 def _get_osd_plan(self
):
602 'data': self
.data
.path
,
603 'data_size': self
.data
.abs_size
,
604 'encryption': self
.encryption
,
607 type_
= self
.fast
.type_
.replace('.', '_')
610 type_
: self
.fast
.path
,
611 '{}_size'.format(type_
): self
.fast
.abs_size
,
616 'block_wal': self
.very_fast
.path
,
617 'block_wal_size': self
.very_fast
.abs_size
,
620 plan
.update({'osd_id': self
.id_
})
623 def get_args(self
, defaults
):
624 my_defaults
= defaults
.copy()
625 my_defaults
.update(self
._get
_osd
_plan
())
631 report
+= templates
.osd_reused_id
.format(
634 report
+= templates
.osd_encryption
.format(
636 report
+= templates
.osd_component
.format(
637 _type
=self
.data
.type_
,
639 size
=self
.data
.abs_size
,
640 percent
=self
.data
.rel_size
)
642 report
+= templates
.osd_component
.format(
643 _type
=self
.fast
.type_
,
645 size
=self
.fast
.abs_size
,
646 percent
=self
.fast
.rel_size
)
648 report
+= templates
.osd_component
.format(
649 _type
=self
.very_fast
.type_
,
650 path
=self
.very_fast
.path
,
651 size
=self
.very_fast
.abs_size
,
652 percent
=self
.very_fast
.rel_size
)
655 def report_json(self
):
656 # cast all values to string so that the report can be dumped in to
658 return {k
: str(v
) for k
, v
in self
._get
_osd
_plan
().items()}