4 from ceph
.deployment
.inventory
import Device
5 from ceph
.deployment
.service_spec
import (
11 from ceph
.deployment
.hostspec
import SpecValidationError
14 from typing
import Optional
, List
, Dict
, Any
, Union
19 class OSDMethod(str, enum
.Enum
):
23 def to_json(self
) -> str:
27 class DeviceSelection(object):
29 Used within :class:`ceph.deployment.drive_group.DriveGroupSpec` to specify the devices
30 used by the Drive Group.
32 Any attributes (even none) can be included in the device
33 specification structure.
36 _supported_filters
= [
37 "actuators", "paths", "size", "vendor", "model", "rotational", "limit", "all"
41 actuators
=None, # type: Optional[int]
42 paths
=None, # type: Optional[List[Dict[str, str]]]
43 model
=None, # type: Optional[str]
44 size
=None, # type: Optional[str]
45 rotational
=None, # type: Optional[bool]
46 limit
=None, # type: Optional[int]
47 vendor
=None, # type: Optional[str]
48 all
=False, # type: bool
51 ephemeral drive group device specification
53 self
.actuators
= actuators
55 #: List of Device objects for devices paths.
61 if isinstance(device
, dict):
62 path
: str = device
.get("path", '')
63 self
.paths
.append(Device(path
, crush_device_class
=device
.get("crush_device_class", None))) # noqa E501
65 self
.paths
.append(Device(str(device
)))
67 #: A wildcard string. e.g: "SDD*" or "SanDisk SD8SN8U5"
70 #: Match on the VENDOR property of the drive
73 #: Size specification of format LOW:HIGH.
74 #: Can also take the form :HIGH, LOW:
75 #: or an exact value (as ceph-volume inventory reports)
76 self
.size
: Optional
[str] = size
78 #: is the drive rotating or not
79 self
.rotational
= rotational
81 #: Limit the number of devices added to this Drive Group. Devices
82 #: are used from top to bottom in the output of ``ceph-volume inventory``
85 #: Matches all devices. Can only be used for data devices
88 def validate(self
, name
: str) -> None:
89 props
= [self
.actuators
, self
.model
, self
.vendor
, self
.size
,
90 self
.rotational
] # type: List[Any]
91 if self
.paths
and any(p
is not None for p
in props
):
92 raise DriveGroupValidationError(
94 'device selection: `paths` and other parameters are mutually exclusive')
95 is_empty
= not any(p
is not None and p
!= [] for p
in [self
.paths
] + props
)
96 if not self
.all
and is_empty
:
97 raise DriveGroupValidationError(name
, 'device selection cannot be empty')
99 if self
.all
and not is_empty
:
100 raise DriveGroupValidationError(
102 'device selection: `all` and other parameters are mutually exclusive. {}'.format(
106 def from_json(cls
, device_spec
):
107 # type: (dict) -> Optional[DeviceSelection]
110 for applied_filter
in list(device_spec
.keys()):
111 if applied_filter
not in cls
._supported
_filters
:
112 raise KeyError(applied_filter
)
114 return cls(**device_spec
)
117 # type: () -> Dict[str, Any]
118 ret
: Dict
[str, Any
] = {}
120 ret
['paths'] = [p
.path
for p
in self
.paths
]
122 ret
['model'] = self
.model
124 ret
['vendor'] = self
.vendor
126 ret
['size'] = self
.size
127 if self
.rotational
is not None:
128 ret
['rotational'] = self
.rotational
130 ret
['limit'] = self
.limit
132 ret
['all'] = self
.all
136 def __repr__(self
) -> str:
138 key
for key
in self
._supported
_filters
+ ['limit'] if getattr(self
, key
) is not None
140 if 'paths' in keys
and self
.paths
== []:
142 return "DeviceSelection({})".format(
143 ', '.join('{}={}'.format(key
, repr(getattr(self
, key
))) for key
in keys
)
146 def __eq__(self
, other
: Any
) -> bool:
147 return repr(self
) == repr(other
)
150 class DriveGroupValidationError(SpecValidationError
):
152 Defining an exception here is a bit problematic, cause you cannot properly catch it,
153 if it was raised in a different mgr module.
156 def __init__(self
, name
: Optional
[str], msg
: str):
157 name
= name
or "<unnamed>"
158 super(DriveGroupValidationError
, self
).__init
__(
159 f
'Failed to validate OSD spec "{name}": {msg}')
162 class DriveGroupSpec(ServiceSpec
):
164 Describe a drive group in the same form that ceph-volume
168 _supported_features
= [
169 "encrypted", "block_wal_size", "osds_per_device",
170 "db_slots", "wal_slots", "block_db_size", "placement", "service_id", "service_type",
171 "data_devices", "db_devices", "wal_devices", "journal_devices",
172 "data_directories", "osds_per_device", "objectstore", "osd_id_claims",
173 "journal_size", "unmanaged", "filter_logic", "preview_only", "extra_container_args",
174 "extra_entrypoint_args", "data_allocate_fraction", "method", "crush_device_class", "config",
178 placement
=None, # type: Optional[PlacementSpec]
179 service_id
=None, # type: Optional[str]
180 data_devices
=None, # type: Optional[DeviceSelection]
181 db_devices
=None, # type: Optional[DeviceSelection]
182 wal_devices
=None, # type: Optional[DeviceSelection]
183 journal_devices
=None, # type: Optional[DeviceSelection]
184 data_directories
=None, # type: Optional[List[str]]
185 osds_per_device
=None, # type: Optional[int]
186 objectstore
='bluestore', # type: str
187 encrypted
=False, # type: bool
188 db_slots
=None, # type: Optional[int]
189 wal_slots
=None, # type: Optional[int]
190 osd_id_claims
=None, # type: Optional[Dict[str, List[str]]]
191 block_db_size
=None, # type: Union[int, str, None]
192 block_wal_size
=None, # type: Union[int, str, None]
193 journal_size
=None, # type: Union[int, str, None]
194 service_type
=None, # type: Optional[str]
195 unmanaged
=False, # type: bool
196 filter_logic
='AND', # type: str
197 preview_only
=False, # type: bool
198 extra_container_args
: Optional
[GeneralArgList
] = None,
199 extra_entrypoint_args
: Optional
[GeneralArgList
] = None,
200 data_allocate_fraction
=None, # type: Optional[float]
201 method
=None, # type: Optional[OSDMethod]
202 config
=None, # type: Optional[Dict[str, str]]
203 custom_configs
=None, # type: Optional[List[CustomConfig]]
204 crush_device_class
=None, # type: Optional[str]
206 assert service_type
is None or service_type
== 'osd'
207 super(DriveGroupSpec
, self
).__init
__('osd', service_id
=service_id
,
211 preview_only
=preview_only
,
212 extra_container_args
=extra_container_args
,
213 extra_entrypoint_args
=extra_entrypoint_args
,
214 custom_configs
=custom_configs
)
216 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
217 self
.data_devices
= data_devices
219 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
220 self
.db_devices
= db_devices
222 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
223 self
.wal_devices
= wal_devices
225 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
226 self
.journal_devices
= journal_devices
228 #: Set (or override) the "bluestore_block_wal_size" value, in bytes
229 self
.block_wal_size
: Union
[int, str, None] = block_wal_size
231 #: Set (or override) the "bluestore_block_db_size" value, in bytes
232 self
.block_db_size
: Union
[int, str, None] = block_db_size
234 #: set journal_size in bytes
235 self
.journal_size
: Union
[int, str, None] = journal_size
237 #: Number of osd daemons per "DATA" device.
238 #: To fully utilize nvme devices multiple osds are required.
239 #: Can be used to split dual-actuator devices across 2 OSDs, by setting the option to 2.
240 self
.osds_per_device
= osds_per_device
242 #: A list of strings, containing paths which should back OSDs
243 self
.data_directories
= data_directories
245 #: ``filestore`` or ``bluestore``
246 self
.objectstore
= objectstore
248 #: ``true`` or ``false``
249 self
.encrypted
= encrypted
251 #: How many OSDs per DB device
252 self
.db_slots
= db_slots
254 #: How many OSDs per WAL device
255 self
.wal_slots
= wal_slots
257 #: Optional: mapping of host -> List of osd_ids that should be replaced
258 #: See :ref:`orchestrator-osd-replace`
259 self
.osd_id_claims
= osd_id_claims
or dict()
261 #: The logic gate we use to match disks with filters.
263 self
.filter_logic
= filter_logic
.upper()
265 #: If this should be treated as a 'preview' spec
266 self
.preview_only
= preview_only
268 #: Allocate a fraction of the data device (0,1.0]
269 self
.data_allocate_fraction
= data_allocate_fraction
273 #: Crush device class to assign to OSDs
274 self
.crush_device_class
= crush_device_class
277 def _from_json_impl(cls
, json_drive_group
):
278 # type: (dict) -> DriveGroupSpec
280 Initialize 'Drive group' structure
282 :param json_drive_group: A valid json string with a Drive Group
285 args
: Dict
[str, Any
] = json_drive_group
.copy()
286 # legacy json (pre Octopus)
287 if 'host_pattern' in args
and 'placement' not in args
:
288 args
['placement'] = {'host_pattern': args
['host_pattern']}
289 del args
['host_pattern']
291 s_id
= args
.get('service_id', '<unnamed>')
293 # spec: was not mandatory in octopus
295 args
['spec'].update(cls
._drive
_group
_spec
_from
_json
(s_id
, args
['spec']))
296 args
.update(cls
._drive
_group
_spec
_from
_json
(
297 s_id
, {k
: v
for k
, v
in args
.items() if k
!= 'spec'}))
299 return super(DriveGroupSpec
, cls
)._from
_json
_impl
(args
)
302 def _drive_group_spec_from_json(cls
, name
: str, json_drive_group
: dict) -> dict:
303 for applied_filter
in list(json_drive_group
.keys()):
304 if applied_filter
not in cls
._supported
_features
:
305 raise DriveGroupValidationError(
307 "Feature `{}` is not supported".format(applied_filter
))
310 def to_selection(key
: str, vals
: dict) -> Optional
[DeviceSelection
]:
312 return DeviceSelection
.from_json(vals
)
313 except KeyError as e
:
314 raise DriveGroupValidationError(
316 f
"Filtering for `{e.args[0]}` is not supported")
318 args
= {k
: (to_selection(k
, v
) if k
.endswith('_devices') else v
) for k
, v
in
319 json_drive_group
.items()}
321 raise DriveGroupValidationError(name
, "Didn't find drive selections")
323 except (KeyError, TypeError) as e
:
324 raise DriveGroupValidationError(name
, str(e
))
328 super(DriveGroupSpec
, self
).validate()
330 if self
.placement
.is_empty():
331 raise DriveGroupValidationError(self
.service_id
, '`placement` required')
333 if self
.data_devices
is None:
334 raise DriveGroupValidationError(self
.service_id
, "`data_devices` element is required.")
336 specs_names
= "data_devices db_devices wal_devices journal_devices".split()
337 specs
= dict(zip(specs_names
, [getattr(self
, k
) for k
in specs_names
]))
338 for k
, s
in [ks
for ks
in specs
.items() if ks
[1] is not None]:
340 s
.validate(f
'{self.service_id}.{k}')
341 for s
in filter(None, [self
.db_devices
, self
.wal_devices
, self
.journal_devices
]):
343 raise DriveGroupValidationError(
345 "`all` is only allowed for data_devices")
347 if self
.objectstore
not in ('bluestore'):
348 raise DriveGroupValidationError(self
.service_id
,
349 f
"{self.objectstore} is not supported. Must be "
350 f
"one of ('bluestore')")
352 if self
.block_wal_size
is not None and type(self
.block_wal_size
) not in [int, str]:
353 raise DriveGroupValidationError(
355 'block_wal_size must be of type int or string')
356 if self
.block_db_size
is not None and type(self
.block_db_size
) not in [int, str]:
357 raise DriveGroupValidationError(
359 'block_db_size must be of type int or string')
360 if self
.journal_size
is not None and type(self
.journal_size
) not in [int, str]:
361 raise DriveGroupValidationError(
363 'journal_size must be of type int or string')
365 if self
.filter_logic
not in ['AND', 'OR']:
366 raise DriveGroupValidationError(
368 'filter_logic must be either <AND> or <OR>')
370 if self
.method
not in [None, 'lvm', 'raw']:
371 raise DriveGroupValidationError(
373 'method must be one of None, lvm, raw')
374 if self
.method
== 'raw' and self
.objectstore
== 'filestore':
375 raise DriveGroupValidationError(
377 'method raw only supports bluestore')
379 if self
.data_devices
.paths
is not None:
380 for device
in list(self
.data_devices
.paths
):
382 raise DriveGroupValidationError(self
.service_id
, 'Device path cannot be empty') # noqa E501
385 yaml
.add_representer(DriveGroupSpec
, DriveGroupSpec
.yaml_representer
)