]>
Commit | Line | Data |
---|---|---|
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.sys_api['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.sys_api['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.sys_api['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.sys_api['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 | args.filtered_devices = {} | |
102 | if used_devices: | |
103 | for device in used_devices: | |
104 | args.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 | args.filtered_devices[last_device.abspath] = {"reasons": [reason]} | |
113 | logger.info(reason + ": %s" % last_device.abspath) | |
114 | unused_devices = [] | |
115 | ||
116 | return unused_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 | self.argv = argv | |
140 | ||
141 | def get_devices(self): | |
142 | # remove devices with partitions | |
143 | devices = [(device, details) for device, details in | |
144 | disk.get_devices().items() if details.get('partitions') == {}] | |
145 | size_sort = lambda x: (x[0], x[1]['size']) | |
146 | return device_formatter(sorted(devices, key=size_sort)) | |
147 | ||
148 | def print_help(self): | |
149 | return self._help.format( | |
150 | detected_devices=self.get_devices(), | |
151 | ) | |
152 | ||
153 | def report(self, args): | |
154 | strategy = self._get_strategy(args) | |
155 | if args.format == 'pretty': | |
156 | strategy.report_pretty() | |
157 | elif args.format == 'json': | |
158 | strategy.report_json() | |
159 | else: | |
160 | raise RuntimeError('report format must be "pretty" or "json"') | |
161 | ||
162 | def execute(self, args): | |
163 | strategy = self._get_strategy(args) | |
164 | if not args.yes: | |
165 | strategy.report_pretty() | |
166 | terminal.info('The above OSDs would be created if the operation continues') | |
167 | if not prompt_bool('do you want to proceed? (yes/no)'): | |
168 | devices = ','.join([device.abspath for device in args.devices]) | |
169 | terminal.error('aborting OSD provisioning for %s' % devices) | |
170 | raise SystemExit(0) | |
171 | ||
172 | strategy.execute() | |
173 | ||
174 | def _get_strategy(self, args): | |
175 | strategy = get_strategy(args, args.devices) | |
176 | unused_devices = filter_devices(args) | |
177 | if not unused_devices and not args.format == 'json': | |
178 | # report nothing changed | |
179 | mlogger.info("All devices are already used by ceph. No OSDs will be created.") | |
180 | raise SystemExit(0) | |
181 | else: | |
182 | new_strategy = get_strategy(args, unused_devices) | |
183 | if new_strategy and strategy != new_strategy: | |
184 | mlogger.error("Aborting because strategy changed from %s to %s after filtering" % (strategy.type(), new_strategy.type())) | |
185 | raise SystemExit(1) | |
186 | ||
187 | return strategy(unused_devices, args) | |
188 | ||
189 | @decorators.needs_root | |
190 | def main(self): | |
191 | parser = argparse.ArgumentParser( | |
192 | prog='ceph-volume lvm batch', | |
193 | formatter_class=argparse.RawDescriptionHelpFormatter, | |
194 | description=self.print_help(), | |
195 | ) | |
196 | ||
197 | parser.add_argument( | |
198 | 'devices', | |
199 | metavar='DEVICES', | |
200 | nargs='*', | |
201 | type=arg_validators.ValidDevice(), | |
202 | default=[], | |
203 | help='Devices to provision OSDs', | |
204 | ) | |
205 | parser.add_argument( | |
206 | '--bluestore', | |
207 | action='store_true', | |
208 | help='bluestore objectstore (default)', | |
209 | ) | |
210 | parser.add_argument( | |
211 | '--filestore', | |
212 | action='store_true', | |
213 | help='filestore objectstore', | |
214 | ) | |
215 | parser.add_argument( | |
216 | '--report', | |
217 | action='store_true', | |
218 | help='Autodetect the objectstore by inspecting the OSD', | |
219 | ) | |
220 | parser.add_argument( | |
221 | '--yes', | |
222 | action='store_true', | |
223 | help='Avoid prompting for confirmation when provisioning', | |
224 | ) | |
225 | parser.add_argument( | |
226 | '--format', | |
227 | help='output format, defaults to "pretty"', | |
228 | default='pretty', | |
229 | choices=['json', 'pretty'], | |
230 | ) | |
231 | parser.add_argument( | |
232 | '--dmcrypt', | |
233 | action='store_true', | |
234 | help='Enable device encryption via dm-crypt', | |
235 | ) | |
236 | parser.add_argument( | |
237 | '--crush-device-class', | |
238 | dest='crush_device_class', | |
239 | help='Crush device class to assign this OSD to', | |
240 | ) | |
241 | parser.add_argument( | |
242 | '--no-systemd', | |
243 | dest='no_systemd', | |
244 | action='store_true', | |
245 | help='Skip creating and enabling systemd units and starting OSD services', | |
246 | ) | |
247 | parser.add_argument( | |
248 | '--osds-per-device', | |
249 | type=int, | |
250 | default=1, | |
251 | help='Provision more than 1 (the default) OSD per device', | |
252 | ) | |
253 | parser.add_argument( | |
254 | '--block-db-size', | |
255 | type=int, | |
256 | help='Set (or override) the "bluestore_block_db_size" value, in bytes' | |
257 | ) | |
258 | parser.add_argument( | |
259 | '--journal-size', | |
260 | type=int, | |
261 | help='Override the "osd_journal_size" value, in megabytes' | |
262 | ) | |
263 | parser.add_argument( | |
264 | '--prepare', | |
265 | action='store_true', | |
266 | help='Only prepare all OSDs, do not activate', | |
267 | ) | |
268 | args = parser.parse_args(self.argv) | |
269 | ||
270 | if not args.devices: | |
271 | return parser.print_help() | |
272 | ||
273 | # Default to bluestore here since defaulting it in add_argument may | |
274 | # cause both to be True | |
275 | if not args.bluestore and not args.filestore: | |
276 | args.bluestore = True | |
277 | ||
278 | if args.report: | |
279 | self.report(args) | |
280 | else: | |
281 | self.execute(args) |