3 from textwrap
import dedent
4 from ceph_volume
import terminal
, decorators
5 from ceph_volume
.util
import disk
, prompt_bool
6 from ceph_volume
.util
import arg_validators
7 from . import strategies
9 mlogger
= terminal
.MultiLogger(__name__
)
10 logger
= logging
.getLogger(__name__
)
13 device_list_template
= """
14 * {path: <25} {size: <10} {state}"""
17 def device_formatter(devices
):
19 for path
, details
in devices
:
20 lines
.append(device_list_template
.format(
21 path
=path
, size
=details
['human_readable_size'],
22 state
='solid' if details
['rotational'] == '0' else 'rotational')
28 # Scenario filtering/detection
29 def bluestore_single_type(device_facts
):
31 Detect devices that are just HDDs or solid state so that a 1:1
32 device-to-osd provisioning can be done
34 types
= [device
.rotational
for device
in device_facts
]
35 if len(set(types
)) == 1:
36 return strategies
.bluestore
.SingleType
39 def bluestore_mixed_type(device_facts
):
41 Detect if devices are HDDs as well as solid state so that block.db can be
42 placed in solid devices while data is kept in the spinning drives.
44 types
= [device
.rotational
for device
in device_facts
]
45 if len(set(types
)) > 1:
46 return strategies
.bluestore
.MixedType
49 def filestore_single_type(device_facts
):
51 Detect devices that are just HDDs or solid state so that a 1:1
52 device-to-osd provisioning can be done, keeping the journal on the OSD
54 types
= [device
.rotational
for device
in device_facts
]
55 if len(set(types
)) == 1:
56 return strategies
.filestore
.SingleType
59 def filestore_mixed_type(device_facts
):
61 Detect if devices are HDDs as well as solid state so that the journal can be
62 placed in solid devices while data is kept in the spinning drives.
64 types
= [device
.rotational
for device
in device_facts
]
65 if len(set(types
)) > 1:
66 return strategies
.filestore
.MixedType
69 def get_strategy(args
, devices
):
71 Given a set of devices as input, go through the different detection
72 mechanisms to narrow down on a strategy to use. The strategies are 4 in
75 * Single device type on Bluestore
76 * Mixed device types on Bluestore
77 * Single device type on Filestore
78 * Mixed device types on Filestore
80 When the function matches to a scenario it returns the strategy class. This
81 allows for dynamic loading of the conditions needed for each scenario, with
84 bluestore_strategies
= [bluestore_mixed_type
, bluestore_single_type
]
85 filestore_strategies
= [filestore_mixed_type
, filestore_single_type
]
87 strategies
= bluestore_strategies
89 strategies
= filestore_strategies
91 for strategy
in strategies
:
92 backend
= strategy(devices
)
97 def filter_devices(args
):
98 unused_devices
= [device
for device
in args
.devices
if not device
.used_by_ceph
]
99 # only data devices, journals can be reused
100 used_devices
= [device
.abspath
for device
in args
.devices
if device
.used_by_ceph
]
101 filtered_devices
= {}
103 for device
in used_devices
:
104 filtered_devices
[device
] = {"reasons": ["Used by ceph as a data device already"]}
105 logger
.info("Ignoring devices already used by ceph: %s" % ", ".join(used_devices
))
106 if len(unused_devices
) == 1:
107 last_device
= unused_devices
[0]
108 if not last_device
.rotational
and last_device
.is_lvm_member
:
110 reason
= "Used by ceph as a %s already and there are no devices left for data/block" % (
111 last_device
.lvs
[0].tags
.get("ceph.type"),
114 reason
= "Disk is an LVM member already, skipping"
115 filtered_devices
[last_device
.abspath
] = {"reasons": [reason
]}
116 logger
.info(reason
+ ": %s" % last_device
.abspath
)
119 return unused_devices
, filtered_devices
124 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
127 Automatically size devices ready for OSD provisioning based on default strategies.
134 ceph-volume lvm batch [DEVICE...]
136 Optional reporting on possible outcomes is enabled with --report
138 ceph-volume lvm batch --report [DEVICE...]
141 def __init__(self
, argv
):
142 parser
= argparse
.ArgumentParser(
143 prog
='ceph-volume lvm batch',
144 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
145 description
=self
.print_help(),
152 type=arg_validators
.ValidDevice(),
154 help='Devices to provision OSDs',
159 type=arg_validators
.ValidDevice(),
161 help='Devices to provision OSDs db volumes',
166 type=arg_validators
.ValidDevice(),
168 help='Devices to provision OSDs wal volumes',
173 type=arg_validators
.ValidDevice(),
175 help='Devices to provision OSDs journal volumes',
180 help=('deploy standalone OSDs if rotational and non-rotational drives '
181 'are passed in DEVICES'),
186 help='bluestore objectstore (default)',
191 help='filestore objectstore',
196 help='Autodetect the objectstore by inspecting the OSD',
201 help='Avoid prompting for confirmation when provisioning',
205 help='output format, defaults to "pretty"',
207 choices
=['json', 'pretty'],
212 help='Enable device encryption via dm-crypt',
215 '--crush-device-class',
216 dest
='crush_device_class',
217 help='Crush device class to assign this OSD to',
223 help='Skip creating and enabling systemd units and starting OSD services',
229 help='Provision more than 1 (the default) OSD per device',
234 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
239 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
244 help='Override the "osd_journal_size" value, in megabytes'
249 help='Only prepare all OSDs, do not activate',
255 help='Reuse existing OSD ids',
257 self
.args
= parser
.parse_args(argv
)
259 for dev_list
in ['', 'db_', 'wal_', 'journal_']:
260 setattr(self
, '{}usable'.format(dev_list
), [])
262 def get_devices(self
):
263 # remove devices with partitions
264 devices
= [(device
, details
) for device
, details
in
265 disk
.get_devices().items() if details
.get('partitions') == {}]
266 size_sort
= lambda x
: (x
[0], x
[1]['size'])
267 return device_formatter(sorted(devices
, key
=size_sort
))
269 def print_help(self
):
270 return self
._help
.format(
271 detected_devices
=self
.get_devices(),
275 if self
.args
.format
== 'pretty':
276 self
.strategy
.report_pretty(self
.filtered_devices
)
277 elif self
.args
.format
== 'json':
278 self
.strategy
.report_json(self
.filtered_devices
)
280 raise RuntimeError('report format must be "pretty" or "json"')
283 if not self
.args
.yes
:
284 self
.strategy
.report_pretty(self
.filtered_devices
)
285 terminal
.info('The above OSDs would be created if the operation continues')
286 if not prompt_bool('do you want to proceed? (yes/no)'):
287 devices
= ','.join([device
.abspath
for device
in self
.args
.devices
])
288 terminal
.error('aborting OSD provisioning for %s' % devices
)
291 self
.strategy
.execute()
293 def _get_strategy(self
):
294 strategy
= get_strategy(self
.args
, self
.args
.devices
)
295 unused_devices
, self
.filtered_devices
= filter_devices(self
.args
)
296 if not unused_devices
and not self
.args
.format
== 'json':
297 # report nothing changed
298 mlogger
.info("All devices are already used by ceph. No OSDs will be created.")
301 new_strategy
= get_strategy(self
.args
, unused_devices
)
302 if new_strategy
and strategy
!= new_strategy
:
303 mlogger
.error("Aborting because strategy changed from %s to %s after filtering" % (strategy
.type(), new_strategy
.type()))
306 self
.strategy
= strategy
.with_auto_devices(self
.args
, unused_devices
)
308 @decorators.needs_root
310 if not self
.args
.devices
:
311 return self
.parser
.print_help()
313 # Default to bluestore here since defaulting it in add_argument may
314 # cause both to be True
315 if not self
.args
.bluestore
and not self
.args
.filestore
:
316 self
.args
.bluestore
= True
318 if (self
.args
.no_auto
or self
.args
.db_devices
or
319 self
.args
.journal_devices
or
320 self
.args
.wal_devices
):
321 self
._get
_explicit
_strategy
()
330 def _get_explicit_strategy(self
):
331 self
._filter
_devices
()
332 self
._ensure
_disjoint
_device
_lists
()
333 if self
.args
.bluestore
:
334 if self
.db_usable
or self
.wal_usable
:
335 self
.strategy
= strategies
.bluestore
.MixedType(
341 self
.strategy
= strategies
.bluestore
.SingleType(
345 if self
.journal_usable
:
346 self
.strategy
= strategies
.filestore
.MixedType(
351 self
.strategy
= strategies
.filestore
.SingleType(
356 def _filter_devices(self
):
357 # filter devices by their available property.
358 # TODO: Some devices are rejected in the argparser already. maybe it
359 # makes sense to unifiy this
360 used_reason
= {"reasons": ["Used by ceph already"]}
361 self
.filtered_devices
= {}
362 for dev_list
in ['', 'db_', 'wal_', 'journal_']:
363 dev_list_prop
= '{}devices'.format(dev_list
)
364 if hasattr(self
.args
, dev_list_prop
):
365 usable_dev_list_prop
= '{}usable'.format(dev_list
)
366 devs
= getattr(self
.args
, dev_list_prop
)
367 usable
= [d
for d
in devs
if d
.available
]
368 setattr(self
, usable_dev_list_prop
, usable
)
369 self
.filtered_devices
.update({d
: used_reason
for d
in
370 getattr(self
.args
, dev_list_prop
)
372 # only fail if non-interactive, this iteration concerns
373 # non-data devices, there are usable data devices (or not all
374 # data devices were filtered) and non-data devices were filtered
375 # so in short this branch is not taken if all data devices are
377 if self
.args
.yes
and dev_list
and self
.usable
and devs
!= usable
:
378 err
= '{} devices were filtered in non-interactive mode, bailing out'
379 raise RuntimeError(err
.format(len(devs
) - len(usable
)))
382 def _ensure_disjoint_device_lists(self
):
383 # check that all device lists are disjoint with each other
384 if not(set(self
.usable
).isdisjoint(set(self
.db_usable
)) and
385 set(self
.usable
).isdisjoint(set(self
.wal_usable
)) and
386 set(self
.usable
).isdisjoint(set(self
.journal_usable
)) and
387 set(self
.db_usable
).isdisjoint(set(self
.wal_usable
))):
388 raise Exception('Device lists are not disjoint')