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
=[]):
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(db
).isdisjoint(set(wal
))]):
37 raise Exception('Device lists are not disjoint')
40 def separate_devices_from_lvs(devices
):
44 phys
.append(d
) if d
.is_device
else lvm
.append(d
)
48 def get_physical_osds(devices
, args
):
50 Goes through passed physical devices and assigns OSDs
52 data_slots
= args
.osds_per_device
54 data_slots
= max(args
.data_slots
, args
.osds_per_device
)
55 rel_data_size
= args
.data_allocate_fraction
/ data_slots
56 mlogger
.debug('relative data size: {}'.format(rel_data_size
))
60 dev_size
= dev
.vg_size
[0]
61 abs_size
= disk
.Size(b
=int(dev_size
* rel_data_size
))
62 free_size
= dev
.vg_free
[0]
63 for _
in range(args
.osds_per_device
):
64 if abs_size
> free_size
:
66 free_size
-= abs_size
.b
69 osd_id
= args
.osd_ids
.pop()
70 ret
.append(Batch
.OSD(dev
.path
,
75 '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_name
, vg_devices
in vg_device_map
.items():
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 if vg_name
== 'unused_devices':
126 slots_for_vg
= requested_slots
128 if len(vg_devices
) > 1:
129 slots_for_vg
= len(args
.devices
)
131 slots_for_vg
= len(vg_devices
) * requested_slots
132 dev_size
= dev
.vg_size
[0]
133 # this only looks at the first vg on device, unsure if there is a better
135 abs_size
= disk
.Size(b
=int(dev_size
/ slots_for_vg
))
136 free_size
= dev
.vg_free
[0]
137 relative_size
= int(abs_size
) / dev_size
139 if requested_size
<= abs_size
:
140 abs_size
= requested_size
141 relative_size
= int(abs_size
) / dev_size
144 '{} was requested for {}, but only {} can be fulfilled'.format(
146 '{}_size'.format(type_
),
150 while abs_size
<= free_size
and len(ret
) < new_osds
and occupied_slots
< fast_slots_per_device
:
151 free_size
-= abs_size
.b
153 ret
.append((dev
.path
, relative_size
, abs_size
, requested_slots
))
156 def group_devices_by_vg(devices
):
158 result
['unused_devices'] = []
161 vg_name
= dev
.vgs
[0].name
162 if vg_name
in result
:
163 result
[vg_name
].append(dev
)
165 result
[vg_name
] = [dev
]
167 result
['unused_devices'].append(dev
)
170 def get_lvm_fast_allocs(lvs
):
171 return [("{}/{}".format(d
.vg_name
, d
.lv_name
), 100.0,
172 disk
.Size(b
=int(d
.lvs
[0].lv_size
)), 1) for d
in lvs
if not
173 d
.journal_used_by_ceph
]
178 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
181 Automatically size devices ready for OSD provisioning based on default strategies.
185 ceph-volume lvm batch [DEVICE...]
187 Devices can be physical block devices or LVs.
188 Optional reporting on possible outcomes is enabled with --report
190 ceph-volume lvm batch --report [DEVICE...]
193 def __init__(self
, argv
):
194 parser
= argparse
.ArgumentParser(
195 prog
='ceph-volume lvm batch',
196 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
197 description
=self
._help
,
204 type=arg_validators
.ValidBatchDataDevice(),
206 help='Devices to provision OSDs',
211 type=arg_validators
.ValidBatchDevice(),
213 help='Devices to provision OSDs db volumes',
218 type=arg_validators
.ValidBatchDevice(),
220 help='Devices to provision OSDs wal volumes',
225 help=('deploy multi-device OSDs if rotational and non-rotational drives '
226 'are passed in DEVICES'),
231 action
='store_false',
233 help=('deploy standalone OSDs if rotational and non-rotational drives '
234 'are passed in DEVICES'),
239 help='bluestore objectstore (default)',
244 help='Only report on OSD that would be created and exit',
249 help='Avoid prompting for confirmation when provisioning',
253 help='output format, defaults to "pretty"',
255 choices
=['json', 'json-pretty', 'pretty'],
259 action
=arg_validators
.DmcryptAction
,
260 help='Enable device encryption via dm-crypt',
263 '--crush-device-class',
264 dest
='crush_device_class',
265 help='Crush device class to assign this OSD to',
272 help='Skip creating and enabling systemd units and starting OSD services',
278 help='Provision more than 1 (the default) OSD per device',
283 help=('Provision more than 1 (the default) OSD slot per device'
284 ' if more slots then osds-per-device are specified, slots'
285 'will stay unoccupied'),
288 '--data-allocate-fraction',
289 type=arg_validators
.ValidFraction(),
290 help='Fraction to allocate from data device (0,1.0]',
295 type=disk
.Size
.parse
,
296 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
301 help='Provision slots on DB device, can remain unoccupied'
305 type=disk
.Size
.parse
,
306 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
311 help='Provision slots on WAL device, can remain unoccupied'
316 help='Only prepare all OSDs, do not activate',
322 help='Reuse existing OSD ids',
323 type=arg_validators
.valid_osd_id
325 self
.args
= parser
.parse_args(argv
)
327 for dev_list
in ['', 'db_', 'wal_']:
328 setattr(self
, '{}usable'.format(dev_list
), [])
330 def report(self
, plan
):
331 report
= self
._create
_report
(plan
)
334 def _create_report(self
, plan
):
335 if self
.args
.format
== 'pretty':
337 report
+= templates
.total_osds
.format(total_osds
=len(plan
))
339 report
+= templates
.osd_component_titles
341 report
+= templates
.osd_header
342 report
+= osd
.report()
347 json_report
.append(osd
.report_json())
348 if self
.args
.format
== 'json':
349 return json
.dumps(json_report
)
350 elif self
.args
.format
== 'json-pretty':
351 return json
.dumps(json_report
, indent
=4,
354 def _check_slot_args(self
):
356 checking if -slots args are consistent with other arguments
358 if self
.args
.data_slots
and self
.args
.osds_per_device
:
359 if self
.args
.data_slots
< self
.args
.osds_per_device
:
360 raise ValueError('data_slots is smaller then osds_per_device')
362 def _sort_rotational_disks(self
):
364 Helper for legacy auto behaviour.
365 Sorts drives into rotating and non-rotating, the latter being used for
368 mlogger
.warning('DEPRECATION NOTICE')
369 mlogger
.warning('You are using the legacy automatic disk sorting behavior')
370 mlogger
.warning('The Pacific release will change the default to --no-auto')
373 for d
in self
.args
.devices
:
374 rotating
.append(d
) if d
.rotational
else ssd
.append(d
)
375 if ssd
and not rotating
:
376 # no need for additional sorting, we'll only deploy standalone on ssds
378 self
.args
.devices
= rotating
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
:
389 self
.args
.bluestore
= True
391 if (self
.args
.auto
and not self
.args
.db_devices
and not
392 self
.args
.wal_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
)
401 plan
= self
.get_plan(self
.args
)
407 if not self
.args
.yes
:
409 terminal
.info('The above OSDs would be created if the operation continues')
410 if not prompt_bool('do you want to proceed? (yes/no)'):
411 terminal
.error('aborting OSD provisioning')
416 def _execute(self
, plan
):
417 defaults
= common
.get_default_args()
421 'crush_device_class',
424 defaults
.update({arg
: getattr(self
.args
, arg
) for arg
in global_args
})
426 args
= osd
.get_args(defaults
)
427 if self
.args
.prepare
:
429 p
.safe_prepare(argparse
.Namespace(**args
))
432 c
.create(argparse
.Namespace(**args
))
435 def get_plan(self
, args
):
437 plan
= self
.get_deployment_layout(args
, args
.devices
, args
.db_devices
,
441 def get_deployment_layout(self
, args
, devices
, fast_devices
=[],
442 very_fast_devices
=[]):
444 The methods here are mostly just organization, error reporting and
445 setting up of (default) args. The heavy lifting code for the deployment
446 layout can be found in the static get_*_osds and get_*_fast_allocs
450 phys_devs
, lvm_devs
= separate_devices_from_lvs(devices
)
451 mlogger
.debug(('passed data devices: {} physical,'
452 ' {} LVM').format(len(phys_devs
), len(lvm_devs
)))
454 plan
.extend(get_physical_osds(phys_devs
, args
))
456 plan
.extend(get_lvm_osds(lvm_devs
, args
))
460 mlogger
.info('All data devices are unavailable')
462 requested_osds
= args
.osds_per_device
* len(phys_devs
) + len(lvm_devs
)
465 fast_type
= 'block_db'
466 fast_allocations
= self
.fast_allocations(fast_devices
,
470 if fast_devices
and not fast_allocations
:
471 mlogger
.info('{} fast devices were passed, but none are available'.format(len(fast_devices
)))
473 if fast_devices
and not len(fast_allocations
) == num_osds
:
474 mlogger
.error('{} fast allocations != {} num_osds'.format(
475 len(fast_allocations
), num_osds
))
478 very_fast_allocations
= self
.fast_allocations(very_fast_devices
,
482 if very_fast_devices
and not very_fast_allocations
:
483 mlogger
.info('{} very fast devices were passed, but none are available'.format(len(very_fast_devices
)))
485 if very_fast_devices
and not len(very_fast_allocations
) == num_osds
:
486 mlogger
.error('{} very fast allocations != {} num_osds'.format(
487 len(very_fast_allocations
), num_osds
))
492 osd
.add_fast_device(*fast_allocations
.pop(),
494 if very_fast_devices
and args
.bluestore
:
495 osd
.add_very_fast_device(*very_fast_allocations
.pop())
498 def fast_allocations(self
, devices
, requested_osds
, new_osds
, type_
):
502 phys_devs
, lvm_devs
= separate_devices_from_lvs(devices
)
503 mlogger
.debug(('passed {} devices: {} physical,'
504 ' {} LVM').format(type_
, len(phys_devs
), len(lvm_devs
)))
506 ret
.extend(get_lvm_fast_allocs(lvm_devs
))
508 # fill up uneven distributions across fast devices: 5 osds and 2 fast
509 # devices? create 3 slots on each device rather then deploying
511 slot_divider
= max(1, len(phys_devs
))
512 if (requested_osds
- len(lvm_devs
)) % slot_divider
:
513 fast_slots_per_device
= int((requested_osds
- len(lvm_devs
)) / slot_divider
) + 1
515 fast_slots_per_device
= int((requested_osds
- len(lvm_devs
)) / slot_divider
)
518 ret
.extend(get_physical_fast_allocs(phys_devs
,
520 fast_slots_per_device
,
527 This class simply stores info about to-be-deployed OSDs and provides an
528 easy way to retrieve the necessary create arguments.
530 VolSpec
= namedtuple('VolSpec',
546 self
.data
= self
.VolSpec(path
=data_path
,
552 self
.very_fast
= None
553 self
.encryption
= encryption
554 self
.symlink
= symlink
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 path
= self
.data
.path
608 path
= f
'{self.symlink} -> {self.data.path}'
609 report
+= templates
.osd_component
.format(
610 _type
=self
.data
.type_
,
612 size
=self
.data
.abs_size
,
613 percent
=self
.data
.rel_size
)
615 report
+= templates
.osd_component
.format(
616 _type
=self
.fast
.type_
,
618 size
=self
.fast
.abs_size
,
619 percent
=self
.fast
.rel_size
)
621 report
+= templates
.osd_component
.format(
622 _type
=self
.very_fast
.type_
,
623 path
=self
.very_fast
.path
,
624 size
=self
.very_fast
.abs_size
,
625 percent
=self
.very_fast
.rel_size
)
628 def report_json(self
):
629 # cast all values to string so that the report can be dumped in to
631 return {k
: str(v
) for k
, v
in self
._get
_osd
_plan
().items()}