]> git.proxmox.com Git - ceph.git/blame - ceph/src/python-common/ceph/deployment/drive_group.py
import 15.2.5
[ceph.git] / ceph / src / python-common / ceph / deployment / drive_group.py
CommitLineData
f6b5b4d7
TL
1import yaml
2
9f95a23c
TL
3from ceph.deployment.inventory import Device
4from ceph.deployment.service_spec import ServiceSpecValidationError, ServiceSpec, PlacementSpec
5
6try:
7 from typing import Optional, List, Dict, Any
8except ImportError:
9 pass
10import six
11
12
13class DeviceSelection(object):
14 """
15 Used within :class:`ceph.deployment.drive_group.DriveGroupSpec` to specify the devices
16 used by the Drive Group.
17
18 Any attributes (even none) can be included in the device
19 specification structure.
20 """
21
22 _supported_filters = [
23 "paths", "size", "vendor", "model", "rotational", "limit", "all"
24 ]
25
26 def __init__(self,
27 paths=None, # type: Optional[List[str]]
28 model=None, # type: Optional[str]
29 size=None, # type: Optional[str]
30 rotational=None, # type: Optional[bool]
31 limit=None, # type: Optional[int]
32 vendor=None, # type: Optional[str]
33 all=False, # type: bool
34 ):
35 """
36 ephemeral drive group device specification
37 """
38 #: List of Device objects for devices paths.
39 self.paths = [] if paths is None else [Device(path) for path in paths] # type: List[Device]
40
41 #: A wildcard string. e.g: "SDD*" or "SanDisk SD8SN8U5"
42 self.model = model
43
44 #: Match on the VENDOR property of the drive
45 self.vendor = vendor
46
47 #: Size specification of format LOW:HIGH.
48 #: Can also take the the form :HIGH, LOW:
49 #: or an exact value (as ceph-volume inventory reports)
f6b5b4d7 50 self.size: Optional[str] = size
9f95a23c
TL
51
52 #: is the drive rotating or not
53 self.rotational = rotational
54
55 #: Limit the number of devices added to this Drive Group. Devices
56 #: are used from top to bottom in the output of ``ceph-volume inventory``
57 self.limit = limit
58
59 #: Matches all devices. Can only be used for data devices
60 self.all = all
61
62 self.validate()
63
64 def validate(self):
65 # type: () -> None
66 props = [self.model, self.vendor, self.size, self.rotational] # type: List[Any]
67 if self.paths and any(p is not None for p in props):
68 raise DriveGroupValidationError(
69 'DeviceSelection: `paths` and other parameters are mutually exclusive')
70 is_empty = not any(p is not None and p != [] for p in [self.paths] + props)
71 if not self.all and is_empty:
72 raise DriveGroupValidationError('DeviceSelection cannot be empty')
73
74 if self.all and not is_empty:
75 raise DriveGroupValidationError(
76 'DeviceSelection `all` and other parameters are mutually exclusive. {}'.format(
77 repr(self)))
78
79 @classmethod
80 def from_json(cls, device_spec):
f6b5b4d7 81 # type: (dict) -> Optional[DeviceSelection]
9f95a23c
TL
82 if not device_spec:
83 return # type: ignore
84 for applied_filter in list(device_spec.keys()):
85 if applied_filter not in cls._supported_filters:
86 raise DriveGroupValidationError(
87 "Filtering for <{}> is not supported".format(applied_filter))
88
89 return cls(**device_spec)
90
91 def to_json(self):
92 # type: () -> Dict[str, Any]
f6b5b4d7 93 ret: Dict[str, Any] = {}
9f95a23c 94 if self.paths:
f6b5b4d7
TL
95 ret['paths'] = [p.path for p in self.paths]
96 if self.model:
97 ret['model'] = self.model
98 if self.vendor:
99 ret['vendor'] = self.vendor
100 if self.size:
101 ret['size'] = self.size
102 if self.rotational:
103 ret['rotational'] = self.rotational
104 if self.limit:
105 ret['limit'] = self.limit
106 if self.all:
107 ret['all'] = self.all
108
109 return ret
9f95a23c
TL
110
111 def __repr__(self):
112 keys = [
113 key for key in self._supported_filters + ['limit'] if getattr(self, key) is not None
114 ]
115 if 'paths' in keys and self.paths == []:
116 keys.remove('paths')
117 return "DeviceSelection({})".format(
118 ', '.join('{}={}'.format(key, repr(getattr(self, key))) for key in keys)
119 )
120
121 def __eq__(self, other):
122 return repr(self) == repr(other)
123
124
125class DriveGroupValidationError(ServiceSpecValidationError):
126 """
127 Defining an exception here is a bit problematic, cause you cannot properly catch it,
128 if it was raised in a different mgr module.
129 """
130
131 def __init__(self, msg):
132 super(DriveGroupValidationError, self).__init__('Failed to validate Drive Group: ' + msg)
133
134
135class DriveGroupSpec(ServiceSpec):
136 """
137 Describe a drive group in the same form that ceph-volume
138 understands.
139 """
140
141 _supported_features = [
142 "encrypted", "block_wal_size", "osds_per_device",
143 "db_slots", "wal_slots", "block_db_size", "placement", "service_id", "service_type",
144 "data_devices", "db_devices", "wal_devices", "journal_devices",
145 "data_directories", "osds_per_device", "objectstore", "osd_id_claims",
f6b5b4d7 146 "journal_size", "unmanaged", "filter_logic", "preview_only"
9f95a23c
TL
147 ]
148
149 def __init__(self,
150 placement=None, # type: Optional[PlacementSpec]
151 service_id=None, # type: str
152 data_devices=None, # type: Optional[DeviceSelection]
153 db_devices=None, # type: Optional[DeviceSelection]
154 wal_devices=None, # type: Optional[DeviceSelection]
155 journal_devices=None, # type: Optional[DeviceSelection]
156 data_directories=None, # type: Optional[List[str]]
157 osds_per_device=None, # type: Optional[int]
158 objectstore='bluestore', # type: str
159 encrypted=False, # type: bool
160 db_slots=None, # type: Optional[int]
161 wal_slots=None, # type: Optional[int]
1911f103 162 osd_id_claims=None, # type: Optional[Dict[str, List[str]]]
9f95a23c
TL
163 block_db_size=None, # type: Optional[int]
164 block_wal_size=None, # type: Optional[int]
165 journal_size=None, # type: Optional[int]
166 service_type=None, # type: Optional[str]
1911f103 167 unmanaged=False, # type: bool
f6b5b4d7
TL
168 filter_logic='AND', # type: str
169 preview_only=False, # type: bool
9f95a23c
TL
170 ):
171 assert service_type is None or service_type == 'osd'
1911f103
TL
172 super(DriveGroupSpec, self).__init__('osd', service_id=service_id,
173 placement=placement,
f6b5b4d7
TL
174 unmanaged=unmanaged,
175 preview_only=preview_only)
9f95a23c
TL
176
177 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
178 self.data_devices = data_devices
179
180 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
181 self.db_devices = db_devices
182
183 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
184 self.wal_devices = wal_devices
185
186 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
187 self.journal_devices = journal_devices
188
189 #: Set (or override) the "bluestore_block_wal_size" value, in bytes
190 self.block_wal_size = block_wal_size
191
192 #: Set (or override) the "bluestore_block_db_size" value, in bytes
193 self.block_db_size = block_db_size
194
f6b5b4d7 195 #: set journal_size in bytes
9f95a23c
TL
196 self.journal_size = journal_size
197
198 #: Number of osd daemons per "DATA" device.
199 #: To fully utilize nvme devices multiple osds are required.
200 self.osds_per_device = osds_per_device
201
202 #: A list of strings, containing paths which should back OSDs
203 self.data_directories = data_directories
204
205 #: ``filestore`` or ``bluestore``
206 self.objectstore = objectstore
207
208 #: ``true`` or ``false``
209 self.encrypted = encrypted
210
211 #: How many OSDs per DB device
212 self.db_slots = db_slots
213
214 #: How many OSDs per WAL device
215 self.wal_slots = wal_slots
216
1911f103
TL
217 #: Optional: mapping of host -> List of osd_ids that should be replaced
218 #: See :ref:`orchestrator-osd-replace`
219 self.osd_id_claims = osd_id_claims or dict()
9f95a23c 220
f6b5b4d7
TL
221 #: The logic gate we use to match disks with filters.
222 #: defaults to 'AND'
223 self.filter_logic = filter_logic.upper()
224
225 #: If this should be treated as a 'preview' spec
226 self.preview_only = preview_only
227
9f95a23c
TL
228 @classmethod
229 def _from_json_impl(cls, json_drive_group):
230 # type: (dict) -> DriveGroupSpec
231 """
232 Initialize 'Drive group' structure
233
234 :param json_drive_group: A valid json string with a Drive Group
235 specification
236 """
f6b5b4d7 237 args = {}
9f95a23c
TL
238 # legacy json (pre Octopus)
239 if 'host_pattern' in json_drive_group and 'placement' not in json_drive_group:
240 json_drive_group['placement'] = {'host_pattern': json_drive_group['host_pattern']}
241 del json_drive_group['host_pattern']
242
f6b5b4d7
TL
243 try:
244 args['placement'] = PlacementSpec.from_json(json_drive_group.pop('placement'))
245 except KeyError:
246 raise DriveGroupValidationError('OSD spec needs a `placement` key.')
247
248 args['service_type'] = json_drive_group.pop('service_type', 'osd')
249
250 # service_id was not required in early octopus.
251 args['service_id'] = json_drive_group.pop('service_id', '')
252
253 # spec: was not mandatory in octopus
254 if 'spec' in json_drive_group:
255 args.update(cls._drive_group_spec_from_json(json_drive_group.pop('spec')))
256 else:
257 args.update(cls._drive_group_spec_from_json(json_drive_group))
258
259 return cls(**args)
260
261 @classmethod
262 def _drive_group_spec_from_json(cls, json_drive_group: dict) -> dict:
9f95a23c
TL
263 for applied_filter in list(json_drive_group.keys()):
264 if applied_filter not in cls._supported_features:
265 raise DriveGroupValidationError(
266 "Feature <{}> is not supported".format(applied_filter))
267
268 for key in ('block_wal_size', 'block_db_size', 'journal_size'):
269 if key in json_drive_group:
270 if isinstance(json_drive_group[key], six.string_types):
271 from ceph.deployment.drive_selection import SizeMatcher
272 json_drive_group[key] = SizeMatcher.str_to_byte(json_drive_group[key])
273
9f95a23c
TL
274 try:
275 args = {k: (DeviceSelection.from_json(v) if k.endswith('_devices') else v) for k, v in
276 json_drive_group.items()}
277 if not args:
278 raise DriveGroupValidationError("Didn't find Drivegroup specs")
f6b5b4d7 279 return args
9f95a23c
TL
280 except (KeyError, TypeError) as e:
281 raise DriveGroupValidationError(str(e))
282
283 def validate(self):
284 # type: () -> None
285 super(DriveGroupSpec, self).validate()
286
f6b5b4d7
TL
287 if not self.service_id:
288 raise DriveGroupValidationError('service_id is required')
289
e306af50
TL
290 if not isinstance(self.placement.host_pattern, six.string_types) and \
291 self.placement.host_pattern is not None:
9f95a23c
TL
292 raise DriveGroupValidationError('host_pattern must be of type string')
293
294 specs = [self.data_devices, self.db_devices, self.wal_devices, self.journal_devices]
295 for s in filter(None, specs):
296 s.validate()
297 for s in filter(None, [self.db_devices, self.wal_devices, self.journal_devices]):
f6b5b4d7
TL
298 if s.paths:
299 raise DriveGroupValidationError("`paths` is only allowed for data_devices")
9f95a23c
TL
300 if s.all:
301 raise DriveGroupValidationError("`all` is only allowed for data_devices")
302
f6b5b4d7
TL
303 if self.objectstore not in ('bluestore'):
304 raise DriveGroupValidationError(f"{self.objectstore} is not supported. Must be "
305 f"one of ('bluestore')")
9f95a23c
TL
306
307 if self.block_wal_size is not None and type(self.block_wal_size) != int:
308 raise DriveGroupValidationError('block_wal_size must be of type int')
309 if self.block_db_size is not None and type(self.block_db_size) != int:
310 raise DriveGroupValidationError('block_db_size must be of type int')
311
f6b5b4d7
TL
312 if self.filter_logic not in ['AND', 'OR']:
313 raise DriveGroupValidationError('filter_logic must be either <AND> or <OR>')
314
9f95a23c
TL
315 def __repr__(self):
316 keys = [
317 key for key in self._supported_features if getattr(self, key) is not None
318 ]
319 if 'encrypted' in keys and not self.encrypted:
320 keys.remove('encrypted')
321 if 'objectstore' in keys and self.objectstore == 'bluestore':
322 keys.remove('objectstore')
323 return "DriveGroupSpec(name={}->{})".format(
324 self.service_id,
325 ', '.join('{}={}'.format(key, repr(getattr(self, key))) for key in keys)
326 )
327
9f95a23c
TL
328 def __eq__(self, other):
329 return repr(self) == repr(other)
f6b5b4d7
TL
330
331
332yaml.add_representer(DriveGroupSpec, DriveGroupSpec.yaml_representer)