]>
Commit | Line | Data |
---|---|---|
1adf2230 | 1 | import argparse |
91327a77 | 2 | import logging |
f6b5b4d7 | 3 | import json |
1adf2230 AA |
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 | ||
91327a77 AA |
10 | mlogger = terminal.MultiLogger(__name__) |
11 | logger = logging.getLogger(__name__) | |
12 | ||
1adf2230 AA |
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 | """ | |
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 | ||
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 | """ | |
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 | ||
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 | """ | |
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 | ||
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 | """ | |
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 | 70 | def 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 | ||
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] | |
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 | ||
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): | |
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') |