]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/devices/lvm/batch.py
import 15.2.2 octopus source
[ceph.git] / ceph / src / ceph-volume / ceph_volume / devices / lvm / batch.py
1 import argparse
2 import logging
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
8
9 mlogger = terminal.MultiLogger(__name__)
10 logger = logging.getLogger(__name__)
11
12
13 device_list_template = """
14 * {path: <25} {size: <10} {state}"""
15
16
17 def device_formatter(devices):
18 lines = []
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')
23 )
24
25 return ''.join(lines)
26
27
28 # Scenario filtering/detection
29 def bluestore_single_type(device_facts):
30 """
31 Detect devices that are just HDDs or solid state so that a 1:1
32 device-to-osd provisioning can be done
33 """
34 types = [device.rotational for device in device_facts]
35 if len(set(types)) == 1:
36 return strategies.bluestore.SingleType
37
38
39 def bluestore_mixed_type(device_facts):
40 """
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.
43 """
44 types = [device.rotational for device in device_facts]
45 if len(set(types)) > 1:
46 return strategies.bluestore.MixedType
47
48
49 def filestore_single_type(device_facts):
50 """
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
53 """
54 types = [device.rotational for device in device_facts]
55 if len(set(types)) == 1:
56 return strategies.filestore.SingleType
57
58
59 def filestore_mixed_type(device_facts):
60 """
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.
63 """
64 types = [device.rotational for device in device_facts]
65 if len(set(types)) > 1:
66 return strategies.filestore.MixedType
67
68
69 def get_strategy(args, devices):
70 """
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
73 total:
74
75 * Single device type on Bluestore
76 * Mixed device types on Bluestore
77 * Single device type on Filestore
78 * Mixed device types on Filestore
79
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
82 normalized classes
83 """
84 bluestore_strategies = [bluestore_mixed_type, bluestore_single_type]
85 filestore_strategies = [filestore_mixed_type, filestore_single_type]
86 if args.bluestore:
87 strategies = bluestore_strategies
88 else:
89 strategies = filestore_strategies
90
91 for strategy in strategies:
92 backend = strategy(devices)
93 if backend:
94 return backend
95
96
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 = {}
102 if used_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:
109 if last_device.lvs:
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"),
112 )
113 else:
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)
117 unused_devices = []
118
119 return unused_devices, filtered_devices
120
121
122 class Batch(object):
123
124 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
125
126 _help = dedent("""
127 Automatically size devices ready for OSD provisioning based on default strategies.
128
129 Detected devices:
130 {detected_devices}
131
132 Usage:
133
134 ceph-volume lvm batch [DEVICE...]
135
136 Optional reporting on possible outcomes is enabled with --report
137
138 ceph-volume lvm batch --report [DEVICE...]
139 """)
140
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(),
146 )
147
148 parser.add_argument(
149 'devices',
150 metavar='DEVICES',
151 nargs='*',
152 type=arg_validators.ValidDevice(),
153 default=[],
154 help='Devices to provision OSDs',
155 )
156 parser.add_argument(
157 '--db-devices',
158 nargs='*',
159 type=arg_validators.ValidDevice(),
160 default=[],
161 help='Devices to provision OSDs db volumes',
162 )
163 parser.add_argument(
164 '--wal-devices',
165 nargs='*',
166 type=arg_validators.ValidDevice(),
167 default=[],
168 help='Devices to provision OSDs wal volumes',
169 )
170 parser.add_argument(
171 '--journal-devices',
172 nargs='*',
173 type=arg_validators.ValidDevice(),
174 default=[],
175 help='Devices to provision OSDs journal volumes',
176 )
177 parser.add_argument(
178 '--no-auto',
179 action='store_true',
180 help=('deploy standalone OSDs if rotational and non-rotational drives '
181 'are passed in DEVICES'),
182 )
183 parser.add_argument(
184 '--bluestore',
185 action='store_true',
186 help='bluestore objectstore (default)',
187 )
188 parser.add_argument(
189 '--filestore',
190 action='store_true',
191 help='filestore objectstore',
192 )
193 parser.add_argument(
194 '--report',
195 action='store_true',
196 help='Autodetect the objectstore by inspecting the OSD',
197 )
198 parser.add_argument(
199 '--yes',
200 action='store_true',
201 help='Avoid prompting for confirmation when provisioning',
202 )
203 parser.add_argument(
204 '--format',
205 help='output format, defaults to "pretty"',
206 default='pretty',
207 choices=['json', 'pretty'],
208 )
209 parser.add_argument(
210 '--dmcrypt',
211 action='store_true',
212 help='Enable device encryption via dm-crypt',
213 )
214 parser.add_argument(
215 '--crush-device-class',
216 dest='crush_device_class',
217 help='Crush device class to assign this OSD to',
218 )
219 parser.add_argument(
220 '--no-systemd',
221 dest='no_systemd',
222 action='store_true',
223 help='Skip creating and enabling systemd units and starting OSD services',
224 )
225 parser.add_argument(
226 '--osds-per-device',
227 type=int,
228 default=1,
229 help='Provision more than 1 (the default) OSD per device',
230 )
231 parser.add_argument(
232 '--block-db-size',
233 type=int,
234 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
235 )
236 parser.add_argument(
237 '--block-wal-size',
238 type=int,
239 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
240 )
241 parser.add_argument(
242 '--journal-size',
243 type=int,
244 help='Override the "osd_journal_size" value, in megabytes'
245 )
246 parser.add_argument(
247 '--prepare',
248 action='store_true',
249 help='Only prepare all OSDs, do not activate',
250 )
251 parser.add_argument(
252 '--osd-ids',
253 nargs='*',
254 default=[],
255 help='Reuse existing OSD ids',
256 )
257 self.args = parser.parse_args(argv)
258 self.parser = parser
259 for dev_list in ['', 'db_', 'wal_', 'journal_']:
260 setattr(self, '{}usable'.format(dev_list), [])
261
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))
268
269 def print_help(self):
270 return self._help.format(
271 detected_devices=self.get_devices(),
272 )
273
274 def report(self):
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)
279 else:
280 raise RuntimeError('report format must be "pretty" or "json"')
281
282 def execute(self):
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)
289 raise SystemExit(0)
290
291 self.strategy.execute()
292
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.")
299 raise SystemExit(0)
300 else:
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()))
304 raise SystemExit(1)
305
306 self.strategy = strategy.with_auto_devices(self.args, unused_devices)
307
308 @decorators.needs_root
309 def main(self):
310 if not self.args.devices:
311 return self.parser.print_help()
312
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
317
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()
322 else:
323 self._get_strategy()
324
325 if self.args.report:
326 self.report()
327 else:
328 self.execute()
329
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(
336 self.args,
337 self.usable,
338 self.db_usable,
339 self.wal_usable)
340 else:
341 self.strategy = strategies.bluestore.SingleType(
342 self.args,
343 self.usable)
344 else:
345 if self.journal_usable:
346 self.strategy = strategies.filestore.MixedType(
347 self.args,
348 self.usable,
349 self.journal_usable)
350 else:
351 self.strategy = strategies.filestore.SingleType(
352 self.args,
353 self.usable)
354
355
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)
371 if d.used_by_ceph})
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
376 # filtered
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)))
380
381
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')