]> git.proxmox.com Git - ceph.git/blob - 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
1 import argparse
2 import logging
3 import json
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
9
10 mlogger = terminal.MultiLogger(__name__)
11 logger = logging.getLogger(__name__)
12
13
14 device_list_template = """
15 * {path: <25} {size: <10} {state}"""
16
17
18 def 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
30 def 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 """
35 types = [device.rotational for device in device_facts]
36 if len(set(types)) == 1:
37 return strategies.bluestore.SingleType
38
39
40 def 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 """
45 types = [device.rotational for device in device_facts]
46 if len(set(types)) > 1:
47 return strategies.bluestore.MixedType
48
49
50 def 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 """
55 types = [device.rotational for device in device_facts]
56 if len(set(types)) == 1:
57 return strategies.filestore.SingleType
58
59
60 def 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 """
65 types = [device.rotational for device in device_facts]
66 if len(set(types)) > 1:
67 return strategies.filestore.MixedType
68
69
70 def get_strategy(args, devices):
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:
93 backend = strategy(devices)
94 if backend:
95 return backend
96
97
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 = {}
103 if used_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:
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"
116 filtered_devices[last_device.abspath] = {"reasons": [reason]}
117 logger.info(reason + ": %s" % last_device.abspath)
118 unused_devices = []
119
120 return unused_devices, filtered_devices
121
122
123 class 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):
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 )
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 )
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 )
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 )
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 )
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 )
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 )
274
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()
313
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
318
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):
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(
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)
345 else:
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
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)
372 if d.used_by_ceph})
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:
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": []}))
384 raise SystemExit(0)
385 raise RuntimeError(err.format(len(devs) - len(usable)))
386
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')