]> git.proxmox.com Git - ceph.git/blame - ceph/src/ceph-volume/ceph_volume/devices/lvm/batch.py
import 15.2.5
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / batch.py
CommitLineData
1adf2230 1import argparse
91327a77 2import logging
f6b5b4d7 3import json
1adf2230
AA
4from textwrap import dedent
5from ceph_volume import terminal, decorators
6from ceph_volume.util import disk, prompt_bool
7from ceph_volume.util import arg_validators
8from . import strategies
9
91327a77
AA
10mlogger = terminal.MultiLogger(__name__)
11logger = logging.getLogger(__name__)
12
1adf2230
AA
13
14device_list_template = """
15 * {path: <25} {size: <10} {state}"""
16
17
18def device_formatter(devices):
19 lines = []
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')
24 )
25
26 return ''.join(lines)
27
28
29# Scenario filtering/detection
30def bluestore_single_type(device_facts):
31 """
32 Detect devices that are just HDDs or solid state so that a 1:1
33 device-to-osd provisioning can be done
34 """
81eedcae 35 types = [device.rotational for device in device_facts]
1adf2230
AA
36 if len(set(types)) == 1:
37 return strategies.bluestore.SingleType
38
39
40def bluestore_mixed_type(device_facts):
41 """
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.
44 """
81eedcae 45 types = [device.rotational for device in device_facts]
1adf2230
AA
46 if len(set(types)) > 1:
47 return strategies.bluestore.MixedType
48
49
50def filestore_single_type(device_facts):
51 """
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
54 """
81eedcae 55 types = [device.rotational for device in device_facts]
1adf2230
AA
56 if len(set(types)) == 1:
57 return strategies.filestore.SingleType
58
59
60def filestore_mixed_type(device_facts):
61 """
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.
64 """
81eedcae 65 types = [device.rotational for device in device_facts]
1adf2230
AA
66 if len(set(types)) > 1:
67 return strategies.filestore.MixedType
68
69
91327a77 70def get_strategy(args, devices):
1adf2230
AA
71 """
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
74 total:
75
76 * Single device type on Bluestore
77 * Mixed device types on Bluestore
78 * Single device type on Filestore
79 * Mixed device types on Filestore
80
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
83 normalized classes
84 """
85 bluestore_strategies = [bluestore_mixed_type, bluestore_single_type]
86 filestore_strategies = [filestore_mixed_type, filestore_single_type]
87 if args.bluestore:
88 strategies = bluestore_strategies
89 else:
90 strategies = filestore_strategies
91
92 for strategy in strategies:
91327a77 93 backend = strategy(devices)
1adf2230 94 if backend:
91327a77
AA
95 return backend
96
97
98def 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]
11fdf7f2 102 filtered_devices = {}
91327a77
AA
103 if used_devices:
104 for device in used_devices:
11fdf7f2 105 filtered_devices[device] = {"reasons": ["Used by ceph as a data device already"]}
91327a77
AA
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:
1911f103
TL
110 if last_device.lvs:
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"),
113 )
114 else:
115 reason = "Disk is an LVM member already, skipping"
11fdf7f2 116 filtered_devices[last_device.abspath] = {"reasons": [reason]}
91327a77
AA
117 logger.info(reason + ": %s" % last_device.abspath)
118 unused_devices = []
119
11fdf7f2 120 return unused_devices, filtered_devices
1adf2230
AA
121
122
123class Batch(object):
124
125 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
126
127 _help = dedent("""
128 Automatically size devices ready for OSD provisioning based on default strategies.
129
130 Detected devices:
131 {detected_devices}
132
133 Usage:
134
135 ceph-volume lvm batch [DEVICE...]
136
137 Optional reporting on possible outcomes is enabled with --report
138
139 ceph-volume lvm batch --report [DEVICE...]
140 """)
141
142 def __init__(self, argv):
1adf2230
AA
143 parser = argparse.ArgumentParser(
144 prog='ceph-volume lvm batch',
145 formatter_class=argparse.RawDescriptionHelpFormatter,
146 description=self.print_help(),
147 )
148
149 parser.add_argument(
150 'devices',
151 metavar='DEVICES',
152 nargs='*',
153 type=arg_validators.ValidDevice(),
154 default=[],
155 help='Devices to provision OSDs',
156 )
11fdf7f2
TL
157 parser.add_argument(
158 '--db-devices',
159 nargs='*',
160 type=arg_validators.ValidDevice(),
161 default=[],
162 help='Devices to provision OSDs db volumes',
163 )
164 parser.add_argument(
165 '--wal-devices',
166 nargs='*',
167 type=arg_validators.ValidDevice(),
168 default=[],
169 help='Devices to provision OSDs wal volumes',
170 )
171 parser.add_argument(
172 '--journal-devices',
173 nargs='*',
174 type=arg_validators.ValidDevice(),
175 default=[],
176 help='Devices to provision OSDs journal volumes',
177 )
178 parser.add_argument(
179 '--no-auto',
180 action='store_true',
181 help=('deploy standalone OSDs if rotational and non-rotational drives '
182 'are passed in DEVICES'),
183 )
1adf2230
AA
184 parser.add_argument(
185 '--bluestore',
186 action='store_true',
187 help='bluestore objectstore (default)',
188 )
189 parser.add_argument(
190 '--filestore',
191 action='store_true',
192 help='filestore objectstore',
193 )
194 parser.add_argument(
195 '--report',
196 action='store_true',
197 help='Autodetect the objectstore by inspecting the OSD',
198 )
199 parser.add_argument(
200 '--yes',
201 action='store_true',
202 help='Avoid prompting for confirmation when provisioning',
203 )
204 parser.add_argument(
205 '--format',
206 help='output format, defaults to "pretty"',
207 default='pretty',
208 choices=['json', 'pretty'],
209 )
210 parser.add_argument(
211 '--dmcrypt',
212 action='store_true',
213 help='Enable device encryption via dm-crypt',
214 )
215 parser.add_argument(
216 '--crush-device-class',
217 dest='crush_device_class',
218 help='Crush device class to assign this OSD to',
219 )
220 parser.add_argument(
221 '--no-systemd',
222 dest='no_systemd',
223 action='store_true',
224 help='Skip creating and enabling systemd units and starting OSD services',
225 )
91327a77
AA
226 parser.add_argument(
227 '--osds-per-device',
228 type=int,
229 default=1,
230 help='Provision more than 1 (the default) OSD per device',
231 )
232 parser.add_argument(
233 '--block-db-size',
234 type=int,
235 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
236 )
11fdf7f2
TL
237 parser.add_argument(
238 '--block-wal-size',
239 type=int,
240 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
241 )
91327a77
AA
242 parser.add_argument(
243 '--journal-size',
244 type=int,
245 help='Override the "osd_journal_size" value, in megabytes'
246 )
247 parser.add_argument(
248 '--prepare',
249 action='store_true',
250 help='Only prepare all OSDs, do not activate',
251 )
11fdf7f2
TL
252 parser.add_argument(
253 '--osd-ids',
254 nargs='*',
255 default=[],
256 help='Reuse existing OSD ids',
257 )
258 self.args = parser.parse_args(argv)
259 self.parser = parser
260 for dev_list in ['', 'db_', 'wal_', 'journal_']:
261 setattr(self, '{}usable'.format(dev_list), [])
262
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))
269
270 def print_help(self):
271 return self._help.format(
272 detected_devices=self.get_devices(),
273 )
1adf2230 274
11fdf7f2
TL
275 def report(self):
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)
280 else:
281 raise RuntimeError('report format must be "pretty" or "json"')
282
283 def execute(self):
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)
290 raise SystemExit(0)
291
292 self.strategy.execute()
293
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.")
300 raise SystemExit(0)
301 else:
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()))
305 raise SystemExit(1)
306
307 self.strategy = strategy.with_auto_devices(self.args, unused_devices)
308
309 @decorators.needs_root
310 def main(self):
311 if not self.args.devices:
312 return self.parser.print_help()
1adf2230
AA
313
314 # Default to bluestore here since defaulting it in add_argument may
315 # cause both to be True
11fdf7f2
TL
316 if not self.args.bluestore and not self.args.filestore:
317 self.args.bluestore = True
1adf2230 318
11fdf7f2
TL
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()
323 else:
324 self._get_strategy()
325
326 if self.args.report:
327 self.report()
328 else:
329 self.execute()
330
331 def _get_explicit_strategy(self):
11fdf7f2 332 self._filter_devices()
494da23a 333 self._ensure_disjoint_device_lists()
11fdf7f2
TL
334 if self.args.bluestore:
335 if self.db_usable or self.wal_usable:
336 self.strategy = strategies.bluestore.MixedType(
337 self.args,
338 self.usable,
339 self.db_usable,
340 self.wal_usable)
341 else:
342 self.strategy = strategies.bluestore.SingleType(
343 self.args,
344 self.usable)
1adf2230 345 else:
11fdf7f2
TL
346 if self.journal_usable:
347 self.strategy = strategies.filestore.MixedType(
348 self.args,
349 self.usable,
350 self.journal_usable)
351 else:
352 self.strategy = strategies.filestore.SingleType(
353 self.args,
354 self.usable)
355
356
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
92f5a8d4 361 used_reason = {"reasons": ["Used by ceph already"]}
11fdf7f2
TL
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)
92f5a8d4
TL
367 devs = getattr(self.args, dev_list_prop)
368 usable = [d for d in devs if d.available]
11fdf7f2
TL
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)
372 if d.used_by_ceph})
1911f103
TL
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
377 # filtered
378 if self.args.yes and dev_list and self.usable and devs != usable:
92f5a8d4 379 err = '{} devices were filtered in non-interactive mode, bailing out'
f6b5b4d7
TL
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": []}))
384 raise SystemExit(0)
92f5a8d4
TL
385 raise RuntimeError(err.format(len(devs) - len(usable)))
386
494da23a
TL
387
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')