4 from ceph
.deployment
.inventory
import Device
5 from ceph
.deployment
.service_spec
import ServiceSpec
, PlacementSpec
, CustomConfig
6 from ceph
.deployment
.hostspec
import SpecValidationError
9 from typing
import Optional
, List
, Dict
, Any
, Union
14 class OSDMethod(str, enum
.Enum
):
19 class DeviceSelection(object):
21 Used within :class:`ceph.deployment.drive_group.DriveGroupSpec` to specify the devices
22 used by the Drive Group.
24 Any attributes (even none) can be included in the device
25 specification structure.
28 _supported_filters
= [
29 "paths", "size", "vendor", "model", "rotational", "limit", "all"
33 paths
=None, # type: Optional[List[str]]
34 model
=None, # type: Optional[str]
35 size
=None, # type: Optional[str]
36 rotational
=None, # type: Optional[bool]
37 limit
=None, # type: Optional[int]
38 vendor
=None, # type: Optional[str]
39 all
=False, # type: bool
42 ephemeral drive group device specification
44 #: List of Device objects for devices paths.
45 self
.paths
= [] if paths
is None else [Device(path
) for path
in paths
] # type: List[Device]
47 #: A wildcard string. e.g: "SDD*" or "SanDisk SD8SN8U5"
50 #: Match on the VENDOR property of the drive
53 #: Size specification of format LOW:HIGH.
54 #: Can also take the the form :HIGH, LOW:
55 #: or an exact value (as ceph-volume inventory reports)
56 self
.size
: Optional
[str] = size
58 #: is the drive rotating or not
59 self
.rotational
= rotational
61 #: Limit the number of devices added to this Drive Group. Devices
62 #: are used from top to bottom in the output of ``ceph-volume inventory``
65 #: Matches all devices. Can only be used for data devices
68 def validate(self
, name
: str) -> None:
69 props
= [self
.model
, self
.vendor
, self
.size
, self
.rotational
] # type: List[Any]
70 if self
.paths
and any(p
is not None for p
in props
):
71 raise DriveGroupValidationError(
73 'device selection: `paths` and other parameters are mutually exclusive')
74 is_empty
= not any(p
is not None and p
!= [] for p
in [self
.paths
] + props
)
75 if not self
.all
and is_empty
:
76 raise DriveGroupValidationError(name
, 'device selection cannot be empty')
78 if self
.all
and not is_empty
:
79 raise DriveGroupValidationError(
81 'device selection: `all` and other parameters are mutually exclusive. {}'.format(
85 def from_json(cls
, device_spec
):
86 # type: (dict) -> Optional[DeviceSelection]
89 for applied_filter
in list(device_spec
.keys()):
90 if applied_filter
not in cls
._supported
_filters
:
91 raise KeyError(applied_filter
)
93 return cls(**device_spec
)
96 # type: () -> Dict[str, Any]
97 ret
: Dict
[str, Any
] = {}
99 ret
['paths'] = [p
.path
for p
in self
.paths
]
101 ret
['model'] = self
.model
103 ret
['vendor'] = self
.vendor
105 ret
['size'] = self
.size
106 if self
.rotational
is not None:
107 ret
['rotational'] = self
.rotational
109 ret
['limit'] = self
.limit
111 ret
['all'] = self
.all
115 def __repr__(self
) -> str:
117 key
for key
in self
._supported
_filters
+ ['limit'] if getattr(self
, key
) is not None
119 if 'paths' in keys
and self
.paths
== []:
121 return "DeviceSelection({})".format(
122 ', '.join('{}={}'.format(key
, repr(getattr(self
, key
))) for key
in keys
)
125 def __eq__(self
, other
: Any
) -> bool:
126 return repr(self
) == repr(other
)
129 class DriveGroupValidationError(SpecValidationError
):
131 Defining an exception here is a bit problematic, cause you cannot properly catch it,
132 if it was raised in a different mgr module.
135 def __init__(self
, name
: Optional
[str], msg
: str):
136 name
= name
or "<unnamed>"
137 super(DriveGroupValidationError
, self
).__init
__(
138 f
'Failed to validate OSD spec "{name}": {msg}')
141 class DriveGroupSpec(ServiceSpec
):
143 Describe a drive group in the same form that ceph-volume
147 _supported_features
= [
148 "encrypted", "block_wal_size", "osds_per_device",
149 "db_slots", "wal_slots", "block_db_size", "placement", "service_id", "service_type",
150 "data_devices", "db_devices", "wal_devices", "journal_devices",
151 "data_directories", "osds_per_device", "objectstore", "osd_id_claims",
152 "journal_size", "unmanaged", "filter_logic", "preview_only", "extra_container_args",
153 "data_allocate_fraction", "method", "crush_device_class", "config",
157 placement
=None, # type: Optional[PlacementSpec]
158 service_id
=None, # type: Optional[str]
159 data_devices
=None, # type: Optional[DeviceSelection]
160 db_devices
=None, # type: Optional[DeviceSelection]
161 wal_devices
=None, # type: Optional[DeviceSelection]
162 journal_devices
=None, # type: Optional[DeviceSelection]
163 data_directories
=None, # type: Optional[List[str]]
164 osds_per_device
=None, # type: Optional[int]
165 objectstore
='bluestore', # type: str
166 encrypted
=False, # type: bool
167 db_slots
=None, # type: Optional[int]
168 wal_slots
=None, # type: Optional[int]
169 osd_id_claims
=None, # type: Optional[Dict[str, List[str]]]
170 block_db_size
=None, # type: Union[int, str, None]
171 block_wal_size
=None, # type: Union[int, str, None]
172 journal_size
=None, # type: Union[int, str, None]
173 service_type
=None, # type: Optional[str]
174 unmanaged
=False, # type: bool
175 filter_logic
='AND', # type: str
176 preview_only
=False, # type: bool
177 extra_container_args
=None, # type: Optional[List[str]]
178 data_allocate_fraction
=None, # type: Optional[float]
179 method
=None, # type: Optional[OSDMethod]
180 crush_device_class
=None, # type: Optional[str]
181 config
=None, # type: Optional[Dict[str, str]]
182 custom_configs
=None, # type: Optional[List[CustomConfig]]
184 assert service_type
is None or service_type
== 'osd'
185 super(DriveGroupSpec
, self
).__init
__('osd', service_id
=service_id
,
189 preview_only
=preview_only
,
190 extra_container_args
=extra_container_args
,
191 custom_configs
=custom_configs
)
193 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
194 self
.data_devices
= data_devices
196 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
197 self
.db_devices
= db_devices
199 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
200 self
.wal_devices
= wal_devices
202 #: A :class:`ceph.deployment.drive_group.DeviceSelection`
203 self
.journal_devices
= journal_devices
205 #: Set (or override) the "bluestore_block_wal_size" value, in bytes
206 self
.block_wal_size
: Union
[int, str, None] = block_wal_size
208 #: Set (or override) the "bluestore_block_db_size" value, in bytes
209 self
.block_db_size
: Union
[int, str, None] = block_db_size
211 #: set journal_size in bytes
212 self
.journal_size
: Union
[int, str, None] = journal_size
214 #: Number of osd daemons per "DATA" device.
215 #: To fully utilize nvme devices multiple osds are required.
216 #: Can be used to split dual-actuator devices across 2 OSDs, by setting the option to 2.
217 self
.osds_per_device
= osds_per_device
219 #: A list of strings, containing paths which should back OSDs
220 self
.data_directories
= data_directories
222 #: ``filestore`` or ``bluestore``
223 self
.objectstore
= objectstore
225 #: ``true`` or ``false``
226 self
.encrypted
= encrypted
228 #: How many OSDs per DB device
229 self
.db_slots
= db_slots
231 #: How many OSDs per WAL device
232 self
.wal_slots
= wal_slots
234 #: Optional: mapping of host -> List of osd_ids that should be replaced
235 #: See :ref:`orchestrator-osd-replace`
236 self
.osd_id_claims
= osd_id_claims
or dict()
238 #: The logic gate we use to match disks with filters.
240 self
.filter_logic
= filter_logic
.upper()
242 #: If this should be treated as a 'preview' spec
243 self
.preview_only
= preview_only
245 #: Allocate a fraction of the data device (0,1.0]
246 self
.data_allocate_fraction
= data_allocate_fraction
250 #: Crush device class to assign to OSDs
251 self
.crush_device_class
= crush_device_class
254 def _from_json_impl(cls
, json_drive_group
):
255 # type: (dict) -> DriveGroupSpec
257 Initialize 'Drive group' structure
259 :param json_drive_group: A valid json string with a Drive Group
262 args
: Dict
[str, Any
] = json_drive_group
.copy()
263 # legacy json (pre Octopus)
264 if 'host_pattern' in args
and 'placement' not in args
:
265 args
['placement'] = {'host_pattern': args
['host_pattern']}
266 del args
['host_pattern']
268 s_id
= args
.get('service_id', '<unnamed>')
270 # spec: was not mandatory in octopus
272 args
['spec'].update(cls
._drive
_group
_spec
_from
_json
(s_id
, args
['spec']))
274 args
.update(cls
._drive
_group
_spec
_from
_json
(s_id
, args
))
276 return super(DriveGroupSpec
, cls
)._from
_json
_impl
(args
)
279 def _drive_group_spec_from_json(cls
, name
: str, json_drive_group
: dict) -> dict:
280 for applied_filter
in list(json_drive_group
.keys()):
281 if applied_filter
not in cls
._supported
_features
:
282 raise DriveGroupValidationError(
284 "Feature `{}` is not supported".format(applied_filter
))
287 def to_selection(key
: str, vals
: dict) -> Optional
[DeviceSelection
]:
289 return DeviceSelection
.from_json(vals
)
290 except KeyError as e
:
291 raise DriveGroupValidationError(
293 f
"Filtering for `{e.args[0]}` is not supported")
295 args
= {k
: (to_selection(k
, v
) if k
.endswith('_devices') else v
) for k
, v
in
296 json_drive_group
.items()}
298 raise DriveGroupValidationError(name
, "Didn't find drive selections")
300 except (KeyError, TypeError) as e
:
301 raise DriveGroupValidationError(name
, str(e
))
305 super(DriveGroupSpec
, self
).validate()
307 if self
.placement
.is_empty():
308 raise DriveGroupValidationError(self
.service_id
, '`placement` required')
310 if self
.data_devices
is None:
311 raise DriveGroupValidationError(self
.service_id
, "`data_devices` element is required.")
313 specs_names
= "data_devices db_devices wal_devices journal_devices".split()
314 specs
= dict(zip(specs_names
, [getattr(self
, k
) for k
in specs_names
]))
315 for k
, s
in [ks
for ks
in specs
.items() if ks
[1] is not None]:
317 s
.validate(f
'{self.service_id}.{k}')
318 for s
in filter(None, [self
.db_devices
, self
.wal_devices
, self
.journal_devices
]):
320 raise DriveGroupValidationError(
322 "`all` is only allowed for data_devices")
324 if self
.objectstore
not in ('bluestore'):
325 raise DriveGroupValidationError(self
.service_id
,
326 f
"{self.objectstore} is not supported. Must be "
327 f
"one of ('bluestore')")
329 if self
.block_wal_size
is not None and type(self
.block_wal_size
) not in [int, str]:
330 raise DriveGroupValidationError(
332 'block_wal_size must be of type int or string')
333 if self
.block_db_size
is not None and type(self
.block_db_size
) not in [int, str]:
334 raise DriveGroupValidationError(
336 'block_db_size must be of type int or string')
337 if self
.journal_size
is not None and type(self
.journal_size
) not in [int, str]:
338 raise DriveGroupValidationError(
340 'journal_size must be of type int or string')
342 if self
.filter_logic
not in ['AND', 'OR']:
343 raise DriveGroupValidationError(
345 'filter_logic must be either <AND> or <OR>')
347 if self
.method
not in [None, 'lvm', 'raw']:
348 raise DriveGroupValidationError(
350 'method must be one of None, lvm, raw')
351 if self
.method
== 'raw' and self
.objectstore
== 'filestore':
352 raise DriveGroupValidationError(
354 'method raw only supports bluestore')
357 yaml
.add_representer(DriveGroupSpec
, DriveGroupSpec
.yaml_representer
)