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
= 1.0 / 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 to small, ignoring'.format(type_
))
106 requested_slots
= fast_slots_per_device
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)
116 if not dev
.available_lvm
:
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
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
127 if requested_size
<= abs_size
:
128 abs_size
= requested_size
131 '{} was requested for {}, but only {} can be fulfilled'.format(
133 '{}_size'.format(type_
),
137 while abs_size
<= free_size
and len(ret
) < new_osds
and occupied_slots
< fast_slots_per_device
:
138 free_size
-= abs_size
.b
140 ret
.append((dev
.path
, relative_size
, abs_size
, requested_slots
))
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
152 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
155 Automatically size devices ready for OSD provisioning based on default strategies.
159 ceph-volume lvm batch [DEVICE...]
161 Devices can be physical block devices or LVs.
162 Optional reporting on possible outcomes is enabled with --report
164 ceph-volume lvm batch --report [DEVICE...]
167 def __init__(self
, argv
):
168 parser
= argparse
.ArgumentParser(
169 prog
='ceph-volume lvm batch',
170 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
171 description
=self
._help
,
178 type=arg_validators
.ValidBatchDevice(),
180 help='Devices to provision OSDs',
185 type=arg_validators
.ValidBatchDevice(),
187 help='Devices to provision OSDs db volumes',
192 type=arg_validators
.ValidBatchDevice(),
194 help='Devices to provision OSDs wal volumes',
199 type=arg_validators
.ValidBatchDevice(),
201 help='Devices to provision OSDs journal volumes',
206 help=('deploy multi-device OSDs if rotational and non-rotational drives '
207 'are passed in DEVICES'),
212 action
='store_false',
214 help=('deploy standalone OSDs if rotational and non-rotational drives '
215 'are passed in DEVICES'),
220 help='bluestore objectstore (default)',
225 help='filestore objectstore',
230 help='Only report on OSD that would be created and exit',
235 help='Avoid prompting for confirmation when provisioning',
239 help='output format, defaults to "pretty"',
241 choices
=['json', 'json-pretty', 'pretty'],
246 help='Enable device encryption via dm-crypt',
249 '--crush-device-class',
250 dest
='crush_device_class',
251 help='Crush device class to assign this OSD to',
257 help='Skip creating and enabling systemd units and starting OSD services',
263 help='Provision more than 1 (the default) OSD per device',
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'),
274 type=disk
.Size
.parse
,
275 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
280 help='Provision slots on DB device, can remain unoccupied'
284 type=disk
.Size
.parse
,
285 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
290 help='Provision slots on WAL device, can remain unoccupied'
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')
300 return disk
.Size
.parse(size
)
303 type=journal_size_in_mb_hack
,
304 help='Override the "osd_journal_size" value, in megabytes'
309 help='Provision slots on journal device, can remain unoccupied'
314 help='Only prepare all OSDs, do not activate',
320 help='Reuse existing OSD ids',
322 self
.args
= parser
.parse_args(argv
)
324 for dev_list
in ['', 'db_', 'wal_', 'journal_']:
325 setattr(self
, '{}usable'.format(dev_list
), [])
327 def report(self
, plan
):
328 report
= self
._create
_report
(plan
)
331 def _create_report(self
, plan
):
332 if self
.args
.format
== 'pretty':
334 report
+= templates
.total_osds
.format(total_osds
=len(plan
))
336 report
+= templates
.osd_component_titles
338 report
+= templates
.osd_header
339 report
+= osd
.report()
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,
351 def _check_slot_args(self
):
353 checking if -slots args are consistent with other arguments
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')
359 def _sort_rotational_disks(self
):
361 Helper for legacy auto behaviour.
362 Sorts drives into rotating and non-rotating, the latter being used for
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')
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
375 self
.args
.devices
= rotating
376 if self
.args
.filestore
:
377 self
.args
.journal_devices
= ssd
379 self
.args
.db_devices
= ssd
381 @decorators.needs_root
383 if not self
.args
.devices
:
384 return self
.parser
.print_help()
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
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
()
395 self
._check
_slot
_args
()
397 ensure_disjoint_device_lists(self
.args
.devices
,
398 self
.args
.db_devices
,
399 self
.args
.wal_devices
,
400 self
.args
.journal_devices
)
402 plan
= self
.get_plan(self
.args
)
408 if not self
.args
.yes
:
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')
417 def _execute(self
, plan
):
418 defaults
= common
.get_default_args()
423 'crush_device_class',
426 defaults
.update({arg
: getattr(self
.args
, arg
) for arg
in global_args
})
428 args
= osd
.get_args(defaults
)
429 if self
.args
.prepare
:
431 p
.safe_prepare(argparse
.Namespace(**args
))
434 c
.create(argparse
.Namespace(**args
))
437 def get_plan(self
, args
):
439 plan
= self
.get_deployment_layout(args
, args
.devices
, args
.db_devices
,
442 plan
= self
.get_deployment_layout(args
, args
.devices
, args
.journal_devices
)
445 def get_deployment_layout(self
, args
, devices
, fast_devices
=[],
446 very_fast_devices
=[]):
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
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
)))
458 plan
.extend(get_physical_osds(phys_devs
, args
))
460 plan
.extend(get_lvm_osds(lvm_devs
, args
))
464 mlogger
.info('All data devices are unavailable')
466 requested_osds
= args
.osds_per_device
* len(phys_devs
) + len(lvm_devs
)
468 fast_type
= 'block_db' if args
.bluestore
else 'journal'
469 fast_allocations
= self
.fast_allocations(fast_devices
,
473 if fast_devices
and not fast_allocations
:
474 mlogger
.info('{} fast devices were passed, but none are available'.format(len(fast_devices
)))
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
))
481 very_fast_allocations
= self
.fast_allocations(very_fast_devices
,
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
)))
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
))
495 osd
.add_fast_device(*fast_allocations
.pop(),
497 if very_fast_devices
and args
.bluestore
:
498 osd
.add_very_fast_device(*very_fast_allocations
.pop())
501 def fast_allocations(self
, devices
, requested_osds
, new_osds
, type_
):
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
)))
509 ret
.extend(get_lvm_fast_allocs(lvm_devs
))
511 # fill up uneven distributions across fast devices: 5 osds and 2 fast
512 # devices? create 3 slots on each device rather then deploying
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
517 fast_slots_per_device
= int((requested_osds
- len(lvm_devs
)) / len(phys_devs
))
520 ret
.extend(get_physical_fast_allocs(phys_devs
,
522 fast_slots_per_device
,
529 This class simply stores info about to-be-deployed OSDs and provides an
530 easy way to retrieve the necessary create arguments.
532 VolSpec
= namedtuple('VolSpec',
547 self
.data
= self
.VolSpec(path
=data_path
,
553 self
.very_fast
= None
554 self
.encryption
= encryption
556 def add_fast_device(self
, path
, rel_size
, abs_size
, slots
, type_
):
557 self
.fast
= self
.VolSpec(path
=path
,
563 def add_very_fast_device(self
, path
, rel_size
, abs_size
, slots
):
564 self
.very_fast
= self
.VolSpec(path
=path
,
570 def _get_osd_plan(self
):
572 'data': self
.data
.path
,
573 'data_size': self
.data
.abs_size
,
574 'encryption': self
.encryption
,
577 type_
= self
.fast
.type_
.replace('.', '_')
580 type_
: self
.fast
.path
,
581 '{}_size'.format(type_
): self
.fast
.abs_size
,
586 'block_wal': self
.very_fast
.path
,
587 'block_wal_size': self
.very_fast
.abs_size
,
590 plan
.update({'osd_id': self
.id_
})
593 def get_args(self
, defaults
):
594 my_defaults
= defaults
.copy()
595 my_defaults
.update(self
._get
_osd
_plan
())
601 report
+= templates
.osd_reused_id
.format(
604 report
+= templates
.osd_encryption
.format(
606 report
+= templates
.osd_component
.format(
607 _type
=self
.data
.type_
,
609 size
=self
.data
.abs_size
,
610 percent
=self
.data
.rel_size
)
612 report
+= templates
.osd_component
.format(
613 _type
=self
.fast
.type_
,
615 size
=self
.fast
.abs_size
,
616 percent
=self
.fast
.rel_size
)
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
)
625 def report_json(self
):
626 # cast all values to string so that the report can be dumped in to
628 return {k
: str(v
) for k
, v
in self
._get
_osd
_plan
().items()}