4 from textwrap
import dedent
5 from ceph_volume
import terminal
, decorators
6 from ceph_volume
.util
import disk
, prompt_bool
7 from ceph_volume
.util
import arg_validators
8 from . import strategies
10 mlogger
= terminal
.MultiLogger(__name__
)
11 logger
= logging
.getLogger(__name__
)
14 device_list_template
= """
15 * {path: <25} {size: <10} {state}"""
18 def device_formatter(devices
):
20 for path
, details
in devices
:
21 lines
.append(device_list_template
.format(
22 path
=path
, size
=details
['human_readable_size'],
23 state
='solid' if details
['rotational'] == '0' else 'rotational')
29 # Scenario filtering/detection
30 def bluestore_single_type(device_facts
):
32 Detect devices that are just HDDs or solid state so that a 1:1
33 device-to-osd provisioning can be done
35 types
= [device
.rotational
for device
in device_facts
]
36 if len(set(types
)) == 1:
37 return strategies
.bluestore
.SingleType
40 def bluestore_mixed_type(device_facts
):
42 Detect if devices are HDDs as well as solid state so that block.db can be
43 placed in solid devices while data is kept in the spinning drives.
45 types
= [device
.rotational
for device
in device_facts
]
46 if len(set(types
)) > 1:
47 return strategies
.bluestore
.MixedType
50 def filestore_single_type(device_facts
):
52 Detect devices that are just HDDs or solid state so that a 1:1
53 device-to-osd provisioning can be done, keeping the journal on the OSD
55 types
= [device
.rotational
for device
in device_facts
]
56 if len(set(types
)) == 1:
57 return strategies
.filestore
.SingleType
60 def filestore_mixed_type(device_facts
):
62 Detect if devices are HDDs as well as solid state so that the journal can be
63 placed in solid devices while data is kept in the spinning drives.
65 types
= [device
.rotational
for device
in device_facts
]
66 if len(set(types
)) > 1:
67 return strategies
.filestore
.MixedType
70 def get_strategy(args
, devices
):
72 Given a set of devices as input, go through the different detection
73 mechanisms to narrow down on a strategy to use. The strategies are 4 in
76 * Single device type on Bluestore
77 * Mixed device types on Bluestore
78 * Single device type on Filestore
79 * Mixed device types on Filestore
81 When the function matches to a scenario it returns the strategy class. This
82 allows for dynamic loading of the conditions needed for each scenario, with
85 bluestore_strategies
= [bluestore_mixed_type
, bluestore_single_type
]
86 filestore_strategies
= [filestore_mixed_type
, filestore_single_type
]
88 strategies
= bluestore_strategies
90 strategies
= filestore_strategies
92 for strategy
in strategies
:
93 backend
= strategy(devices
)
98 def filter_devices(args
):
99 unused_devices
= [device
for device
in args
.devices
if not device
.used_by_ceph
]
100 # only data devices, journals can be reused
101 used_devices
= [device
.abspath
for device
in args
.devices
if device
.used_by_ceph
]
102 filtered_devices
= {}
104 for device
in used_devices
:
105 filtered_devices
[device
] = {"reasons": ["Used by ceph as a data device already"]}
106 logger
.info("Ignoring devices already used by ceph: %s" % ", ".join(used_devices
))
107 if len(unused_devices
) == 1:
108 last_device
= unused_devices
[0]
109 if not last_device
.rotational
and last_device
.is_lvm_member
:
111 reason
= "Used by ceph as a %s already and there are no devices left for data/block" % (
112 last_device
.lvs
[0].tags
.get("ceph.type"),
115 reason
= "Disk is an LVM member already, skipping"
116 filtered_devices
[last_device
.abspath
] = {"reasons": [reason
]}
117 logger
.info(reason
+ ": %s" % last_device
.abspath
)
120 return unused_devices
, filtered_devices
125 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
128 Automatically size devices ready for OSD provisioning based on default strategies.
135 ceph-volume lvm batch [DEVICE...]
137 Optional reporting on possible outcomes is enabled with --report
139 ceph-volume lvm batch --report [DEVICE...]
142 def __init__(self
, argv
):
143 parser
= argparse
.ArgumentParser(
144 prog
='ceph-volume lvm batch',
145 formatter_class
=argparse
.RawDescriptionHelpFormatter
,
146 description
=self
.print_help(),
153 type=arg_validators
.ValidDevice(),
155 help='Devices to provision OSDs',
160 type=arg_validators
.ValidDevice(),
162 help='Devices to provision OSDs db volumes',
167 type=arg_validators
.ValidDevice(),
169 help='Devices to provision OSDs wal volumes',
174 type=arg_validators
.ValidDevice(),
176 help='Devices to provision OSDs journal volumes',
181 help=('deploy standalone OSDs if rotational and non-rotational drives '
182 'are passed in DEVICES'),
187 help='bluestore objectstore (default)',
192 help='filestore objectstore',
197 help='Autodetect the objectstore by inspecting the OSD',
202 help='Avoid prompting for confirmation when provisioning',
206 help='output format, defaults to "pretty"',
208 choices
=['json', 'pretty'],
213 help='Enable device encryption via dm-crypt',
216 '--crush-device-class',
217 dest
='crush_device_class',
218 help='Crush device class to assign this OSD to',
224 help='Skip creating and enabling systemd units and starting OSD services',
230 help='Provision more than 1 (the default) OSD per device',
235 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
240 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
245 help='Override the "osd_journal_size" value, in megabytes'
250 help='Only prepare all OSDs, do not activate',
256 help='Reuse existing OSD ids',
258 self
.args
= parser
.parse_args(argv
)
260 for dev_list
in ['', 'db_', 'wal_', 'journal_']:
261 setattr(self
, '{}usable'.format(dev_list
), [])
263 def get_devices(self
):
264 # remove devices with partitions
265 devices
= [(device
, details
) for device
, details
in
266 disk
.get_devices().items() if details
.get('partitions') == {}]
267 size_sort
= lambda x
: (x
[0], x
[1]['size'])
268 return device_formatter(sorted(devices
, key
=size_sort
))
270 def print_help(self
):
271 return self
._help
.format(
272 detected_devices
=self
.get_devices(),
276 if self
.args
.format
== 'pretty':
277 self
.strategy
.report_pretty(self
.filtered_devices
)
278 elif self
.args
.format
== 'json':
279 self
.strategy
.report_json(self
.filtered_devices
)
281 raise RuntimeError('report format must be "pretty" or "json"')
284 if not self
.args
.yes
:
285 self
.strategy
.report_pretty(self
.filtered_devices
)
286 terminal
.info('The above OSDs would be created if the operation continues')
287 if not prompt_bool('do you want to proceed? (yes/no)'):
288 devices
= ','.join([device
.abspath
for device
in self
.args
.devices
])
289 terminal
.error('aborting OSD provisioning for %s' % devices
)
292 self
.strategy
.execute()
294 def _get_strategy(self
):
295 strategy
= get_strategy(self
.args
, self
.args
.devices
)
296 unused_devices
, self
.filtered_devices
= filter_devices(self
.args
)
297 if not unused_devices
and not self
.args
.format
== 'json':
298 # report nothing changed
299 mlogger
.info("All devices are already used by ceph. No OSDs will be created.")
302 new_strategy
= get_strategy(self
.args
, unused_devices
)
303 if new_strategy
and strategy
!= new_strategy
:
304 mlogger
.error("Aborting because strategy changed from %s to %s after filtering" % (strategy
.type(), new_strategy
.type()))
307 self
.strategy
= strategy
.with_auto_devices(self
.args
, unused_devices
)
309 @decorators.needs_root
311 if not self
.args
.devices
:
312 return self
.parser
.print_help()
314 # Default to bluestore here since defaulting it in add_argument may
315 # cause both to be True
316 if not self
.args
.bluestore
and not self
.args
.filestore
:
317 self
.args
.bluestore
= True
319 if (self
.args
.no_auto
or self
.args
.db_devices
or
320 self
.args
.journal_devices
or
321 self
.args
.wal_devices
):
322 self
._get
_explicit
_strategy
()
331 def _get_explicit_strategy(self
):
332 self
._filter
_devices
()
333 self
._ensure
_disjoint
_device
_lists
()
334 if self
.args
.bluestore
:
335 if self
.db_usable
or self
.wal_usable
:
336 self
.strategy
= strategies
.bluestore
.MixedType(
342 self
.strategy
= strategies
.bluestore
.SingleType(
346 if self
.journal_usable
:
347 self
.strategy
= strategies
.filestore
.MixedType(
352 self
.strategy
= strategies
.filestore
.SingleType(
357 def _filter_devices(self
):
358 # filter devices by their available property.
359 # TODO: Some devices are rejected in the argparser already. maybe it
360 # makes sense to unifiy this
361 used_reason
= {"reasons": ["Used by ceph already"]}
362 self
.filtered_devices
= {}
363 for dev_list
in ['', 'db_', 'wal_', 'journal_']:
364 dev_list_prop
= '{}devices'.format(dev_list
)
365 if hasattr(self
.args
, dev_list_prop
):
366 usable_dev_list_prop
= '{}usable'.format(dev_list
)
367 devs
= getattr(self
.args
, dev_list_prop
)
368 usable
= [d
for d
in devs
if d
.available
]
369 setattr(self
, usable_dev_list_prop
, usable
)
370 self
.filtered_devices
.update({d
: used_reason
for d
in
371 getattr(self
.args
, dev_list_prop
)
373 # only fail if non-interactive, this iteration concerns
374 # non-data devices, there are usable data devices (or not all
375 # data devices were filtered) and non-data devices were filtered
376 # so in short this branch is not taken if all data devices are
378 if self
.args
.yes
and dev_list
and self
.usable
and devs
!= usable
:
379 err
= '{} devices were filtered in non-interactive mode, bailing out'
380 if self
.args
.format
== "json" and self
.args
.report
:
381 # if a json report is requested, report unchanged so idempotency checks
382 # in ceph-ansible will work
383 print(json
.dumps({"changed": False, "osds": [], "vgs": []}))
385 raise RuntimeError(err
.format(len(devs
) - len(usable
)))
388 def _ensure_disjoint_device_lists(self
):
389 # check that all device lists are disjoint with each other
390 if not(set(self
.usable
).isdisjoint(set(self
.db_usable
)) and
391 set(self
.usable
).isdisjoint(set(self
.wal_usable
)) and
392 set(self
.usable
).isdisjoint(set(self
.journal_usable
)) and
393 set(self
.db_usable
).isdisjoint(set(self
.wal_usable
))):
394 raise Exception('Device lists are not disjoint')