]> git.proxmox.com Git - ceph.git/blob - ceph/src/ceph-volume/ceph_volume/devices/lvm/batch.py
import ceph nautilus 14.2.2
[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 reason = "Used by ceph as a %s already and there are no devices left for data/block" % (
110 last_device.lvs[0].tags.get("ceph.type"),
111 )
112 filtered_devices[last_device.abspath] = {"reasons": [reason]}
113 logger.info(reason + ": %s" % last_device.abspath)
114 unused_devices = []
115
116 return unused_devices, filtered_devices
117
118
119 class Batch(object):
120
121 help = 'Automatically size devices for multi-OSD provisioning with minimal interaction'
122
123 _help = dedent("""
124 Automatically size devices ready for OSD provisioning based on default strategies.
125
126 Detected devices:
127 {detected_devices}
128
129 Usage:
130
131 ceph-volume lvm batch [DEVICE...]
132
133 Optional reporting on possible outcomes is enabled with --report
134
135 ceph-volume lvm batch --report [DEVICE...]
136 """)
137
138 def __init__(self, argv):
139 parser = argparse.ArgumentParser(
140 prog='ceph-volume lvm batch',
141 formatter_class=argparse.RawDescriptionHelpFormatter,
142 description=self.print_help(),
143 )
144
145 parser.add_argument(
146 'devices',
147 metavar='DEVICES',
148 nargs='*',
149 type=arg_validators.ValidDevice(),
150 default=[],
151 help='Devices to provision OSDs',
152 )
153 parser.add_argument(
154 '--db-devices',
155 nargs='*',
156 type=arg_validators.ValidDevice(),
157 default=[],
158 help='Devices to provision OSDs db volumes',
159 )
160 parser.add_argument(
161 '--wal-devices',
162 nargs='*',
163 type=arg_validators.ValidDevice(),
164 default=[],
165 help='Devices to provision OSDs wal volumes',
166 )
167 parser.add_argument(
168 '--journal-devices',
169 nargs='*',
170 type=arg_validators.ValidDevice(),
171 default=[],
172 help='Devices to provision OSDs journal volumes',
173 )
174 parser.add_argument(
175 '--no-auto',
176 action='store_true',
177 help=('deploy standalone OSDs if rotational and non-rotational drives '
178 'are passed in DEVICES'),
179 )
180 parser.add_argument(
181 '--bluestore',
182 action='store_true',
183 help='bluestore objectstore (default)',
184 )
185 parser.add_argument(
186 '--filestore',
187 action='store_true',
188 help='filestore objectstore',
189 )
190 parser.add_argument(
191 '--report',
192 action='store_true',
193 help='Autodetect the objectstore by inspecting the OSD',
194 )
195 parser.add_argument(
196 '--yes',
197 action='store_true',
198 help='Avoid prompting for confirmation when provisioning',
199 )
200 parser.add_argument(
201 '--format',
202 help='output format, defaults to "pretty"',
203 default='pretty',
204 choices=['json', 'pretty'],
205 )
206 parser.add_argument(
207 '--dmcrypt',
208 action='store_true',
209 help='Enable device encryption via dm-crypt',
210 )
211 parser.add_argument(
212 '--crush-device-class',
213 dest='crush_device_class',
214 help='Crush device class to assign this OSD to',
215 )
216 parser.add_argument(
217 '--no-systemd',
218 dest='no_systemd',
219 action='store_true',
220 help='Skip creating and enabling systemd units and starting OSD services',
221 )
222 parser.add_argument(
223 '--osds-per-device',
224 type=int,
225 default=1,
226 help='Provision more than 1 (the default) OSD per device',
227 )
228 parser.add_argument(
229 '--block-db-size',
230 type=int,
231 help='Set (or override) the "bluestore_block_db_size" value, in bytes'
232 )
233 parser.add_argument(
234 '--block-wal-size',
235 type=int,
236 help='Set (or override) the "bluestore_block_wal_size" value, in bytes'
237 )
238 parser.add_argument(
239 '--journal-size',
240 type=int,
241 help='Override the "osd_journal_size" value, in megabytes'
242 )
243 parser.add_argument(
244 '--prepare',
245 action='store_true',
246 help='Only prepare all OSDs, do not activate',
247 )
248 parser.add_argument(
249 '--osd-ids',
250 nargs='*',
251 default=[],
252 help='Reuse existing OSD ids',
253 )
254 self.args = parser.parse_args(argv)
255 self.parser = parser
256 for dev_list in ['', 'db_', 'wal_', 'journal_']:
257 setattr(self, '{}usable'.format(dev_list), [])
258
259 def get_devices(self):
260 # remove devices with partitions
261 devices = [(device, details) for device, details in
262 disk.get_devices().items() if details.get('partitions') == {}]
263 size_sort = lambda x: (x[0], x[1]['size'])
264 return device_formatter(sorted(devices, key=size_sort))
265
266 def print_help(self):
267 return self._help.format(
268 detected_devices=self.get_devices(),
269 )
270
271 def report(self):
272 if self.args.format == 'pretty':
273 self.strategy.report_pretty(self.filtered_devices)
274 elif self.args.format == 'json':
275 self.strategy.report_json(self.filtered_devices)
276 else:
277 raise RuntimeError('report format must be "pretty" or "json"')
278
279 def execute(self):
280 if not self.args.yes:
281 self.strategy.report_pretty(self.filtered_devices)
282 terminal.info('The above OSDs would be created if the operation continues')
283 if not prompt_bool('do you want to proceed? (yes/no)'):
284 devices = ','.join([device.abspath for device in self.args.devices])
285 terminal.error('aborting OSD provisioning for %s' % devices)
286 raise SystemExit(0)
287
288 self.strategy.execute()
289
290 def _get_strategy(self):
291 strategy = get_strategy(self.args, self.args.devices)
292 unused_devices, self.filtered_devices = filter_devices(self.args)
293 if not unused_devices and not self.args.format == 'json':
294 # report nothing changed
295 mlogger.info("All devices are already used by ceph. No OSDs will be created.")
296 raise SystemExit(0)
297 else:
298 new_strategy = get_strategy(self.args, unused_devices)
299 if new_strategy and strategy != new_strategy:
300 mlogger.error("Aborting because strategy changed from %s to %s after filtering" % (strategy.type(), new_strategy.type()))
301 raise SystemExit(1)
302
303 self.strategy = strategy.with_auto_devices(self.args, unused_devices)
304
305 @decorators.needs_root
306 def main(self):
307 if not self.args.devices:
308 return self.parser.print_help()
309
310 # Default to bluestore here since defaulting it in add_argument may
311 # cause both to be True
312 if not self.args.bluestore and not self.args.filestore:
313 self.args.bluestore = True
314
315 if (self.args.no_auto or self.args.db_devices or
316 self.args.journal_devices or
317 self.args.wal_devices):
318 self._get_explicit_strategy()
319 else:
320 self._get_strategy()
321
322 if self.args.report:
323 self.report()
324 else:
325 self.execute()
326
327 def _get_explicit_strategy(self):
328 # TODO assert that none of the device lists overlap?
329 self._filter_devices()
330 if self.args.bluestore:
331 if self.db_usable or self.wal_usable:
332 self.strategy = strategies.bluestore.MixedType(
333 self.args,
334 self.usable,
335 self.db_usable,
336 self.wal_usable)
337 else:
338 self.strategy = strategies.bluestore.SingleType(
339 self.args,
340 self.usable)
341 else:
342 if self.journal_usable:
343 self.strategy = strategies.filestore.MixedType(
344 self.args,
345 self.usable,
346 self.journal_usable)
347 else:
348 self.strategy = strategies.filestore.SingleType(
349 self.args,
350 self.usable)
351
352
353 def _filter_devices(self):
354 # filter devices by their available property.
355 # TODO: Some devices are rejected in the argparser already. maybe it
356 # makes sense to unifiy this
357 used_reason = {"reasons": ["Used by ceph as a data device already"]}
358 self.filtered_devices = {}
359 for dev_list in ['', 'db_', 'wal_', 'journal_']:
360 dev_list_prop = '{}devices'.format(dev_list)
361 if hasattr(self.args, dev_list_prop):
362 usable_dev_list_prop = '{}usable'.format(dev_list)
363 usable = [d for d in getattr(self.args, dev_list_prop) if d.available]
364 setattr(self, usable_dev_list_prop, usable)
365 self.filtered_devices.update({d: used_reason for d in
366 getattr(self.args, dev_list_prop)
367 if d.used_by_ceph})