targetcli 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- targetcli/__init__.py +21 -0
- targetcli/targetcli_shell.py +326 -0
- targetcli/targetclid.py +281 -0
- targetcli/ui_backstore.py +787 -0
- targetcli/ui_node.py +212 -0
- targetcli/ui_root.py +329 -0
- targetcli/ui_target.py +1450 -0
- targetcli-3.0.0.dist-info/METADATA +65 -0
- targetcli-3.0.0.dist-info/RECORD +12 -0
- targetcli-3.0.0.dist-info/WHEEL +4 -0
- targetcli-3.0.0.dist-info/entry_points.txt +3 -0
- targetcli-3.0.0.dist-info/licenses/COPYING +175 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Implements the targetcli backstores related UI.
|
|
3
|
+
|
|
4
|
+
This file is part of targetcli.
|
|
5
|
+
Copyright (c) 2011-2013 by Datera, Inc
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
8
|
+
not use this file except in compliance with the License. You may obtain
|
|
9
|
+
a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
15
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
16
|
+
License for the specific language governing permissions and limitations
|
|
17
|
+
under the License.
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
import array
|
|
21
|
+
import fcntl
|
|
22
|
+
import glob
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import stat
|
|
26
|
+
import struct
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from configshell_fb import ExecutionError
|
|
30
|
+
from gi.repository import Gio
|
|
31
|
+
from rtslib_fb import (
|
|
32
|
+
ALUATargetPortGroup,
|
|
33
|
+
BlockStorageObject,
|
|
34
|
+
FileIOStorageObject,
|
|
35
|
+
PSCSIStorageObject,
|
|
36
|
+
RDMCPStorageObject,
|
|
37
|
+
RTSLibError,
|
|
38
|
+
RTSRoot,
|
|
39
|
+
UserBackedStorageObject,
|
|
40
|
+
)
|
|
41
|
+
from rtslib_fb.utils import get_block_type
|
|
42
|
+
|
|
43
|
+
from .ui_node import UINode, UIRTSLibNode
|
|
44
|
+
|
|
45
|
+
default_save_file = "/etc/target/saveconfig.json"
|
|
46
|
+
|
|
47
|
+
alua_rw_params = ['alua_access_state', 'alua_access_status',
|
|
48
|
+
'alua_write_metadata', 'alua_access_type', 'preferred',
|
|
49
|
+
'nonop_delay_msecs', 'trans_delay_msecs',
|
|
50
|
+
'implicit_trans_secs', 'alua_support_offline',
|
|
51
|
+
'alua_support_standby', 'alua_support_transitioning',
|
|
52
|
+
'alua_support_active_nonoptimized',
|
|
53
|
+
'alua_support_unavailable', 'alua_support_active_optimized']
|
|
54
|
+
alua_ro_params = ['tg_pt_gp_id', 'members', 'alua_support_lba_dependent']
|
|
55
|
+
|
|
56
|
+
alua_state_names = {0: 'Active/optimized',
|
|
57
|
+
1: 'Active/non-optimized',
|
|
58
|
+
2: 'Standby',
|
|
59
|
+
3: 'Unavailable',
|
|
60
|
+
4: 'LBA Dependent',
|
|
61
|
+
14: 'Offline',
|
|
62
|
+
15: 'Transitioning'}
|
|
63
|
+
|
|
64
|
+
def human_to_bytes(hsize, kilo=1024):
|
|
65
|
+
'''
|
|
66
|
+
This function converts human-readable amounts of bytes to bytes.
|
|
67
|
+
It understands the following units :
|
|
68
|
+
- B or no unit present for Bytes
|
|
69
|
+
- k, K, kB, KB for kB (kilobytes)
|
|
70
|
+
- m, M, mB, MB for MB (megabytes)
|
|
71
|
+
- g, G, gB, GB for GB (gigabytes)
|
|
72
|
+
- t, T, tB, TB for TB (terabytes)
|
|
73
|
+
|
|
74
|
+
Note: The definition of kilo defaults to 1kB = 1024Bytes.
|
|
75
|
+
Strictly speaking, those should not be called "kB" but "kiB".
|
|
76
|
+
You can override that with the optional kilo parameter.
|
|
77
|
+
|
|
78
|
+
@param hsize: The human-readable version of the Bytes amount to convert
|
|
79
|
+
@type hsize: string or int
|
|
80
|
+
@param kilo: Optional base for the kilo prefix
|
|
81
|
+
@type kilo: int
|
|
82
|
+
@return: An int representing the human-readable string converted to bytes
|
|
83
|
+
'''
|
|
84
|
+
size = hsize.replace('i', '').lower()
|
|
85
|
+
if not re.match("^[0-9]+[k|m|g|t]?[b]?$", size):
|
|
86
|
+
raise RTSLibError(f"Cannot interpret size, wrong format: {hsize}")
|
|
87
|
+
|
|
88
|
+
size = size.rstrip('ib')
|
|
89
|
+
|
|
90
|
+
units = ['k', 'm', 'g', 't']
|
|
91
|
+
try:
|
|
92
|
+
power = units.index(size[-1]) + 1
|
|
93
|
+
except ValueError:
|
|
94
|
+
power = 0
|
|
95
|
+
size = int(size)
|
|
96
|
+
else:
|
|
97
|
+
size = int(size[:-1])
|
|
98
|
+
|
|
99
|
+
return size * (int(kilo) ** power)
|
|
100
|
+
|
|
101
|
+
def bytes_to_human(size):
|
|
102
|
+
kilo = 1024.0
|
|
103
|
+
|
|
104
|
+
# don't use decimal for bytes
|
|
105
|
+
if size < kilo:
|
|
106
|
+
return "%d bytes" % size
|
|
107
|
+
size /= kilo
|
|
108
|
+
|
|
109
|
+
for x in ('KiB', 'MiB', 'GiB', 'TiB', 'PiB'):
|
|
110
|
+
if size < kilo:
|
|
111
|
+
return f"{size:3.1f}{x}"
|
|
112
|
+
size /= kilo
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def complete_path(path, stat_fn):
|
|
116
|
+
filtered = []
|
|
117
|
+
for entry in glob.glob(path + '*'):
|
|
118
|
+
st = os.stat(entry)
|
|
119
|
+
if stat.S_ISDIR(st.st_mode):
|
|
120
|
+
filtered.append(entry + '/')
|
|
121
|
+
elif stat_fn(st.st_mode):
|
|
122
|
+
filtered.append(entry)
|
|
123
|
+
|
|
124
|
+
# Put directories at the end
|
|
125
|
+
return sorted(filtered,
|
|
126
|
+
key=lambda s: '~' + s if s.endswith('/') else s)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class UIALUATargetPortGroup(UIRTSLibNode):
|
|
130
|
+
'''
|
|
131
|
+
A generic UI for ALUATargetPortGroup objects.
|
|
132
|
+
'''
|
|
133
|
+
def __init__(self, alua_tpg, parent):
|
|
134
|
+
name = alua_tpg.name
|
|
135
|
+
super().__init__(name, alua_tpg, parent)
|
|
136
|
+
self.refresh()
|
|
137
|
+
|
|
138
|
+
for param in alua_rw_params:
|
|
139
|
+
self.define_config_group_param("alua", param, 'string')
|
|
140
|
+
|
|
141
|
+
for param in alua_ro_params:
|
|
142
|
+
self.define_config_group_param("alua", param, 'string', writable=False)
|
|
143
|
+
|
|
144
|
+
def ui_getgroup_alua(self, alua_attr):
|
|
145
|
+
return getattr(self.rtsnode, alua_attr)
|
|
146
|
+
|
|
147
|
+
def ui_setgroup_alua(self, alua_attr, value):
|
|
148
|
+
self.assert_root()
|
|
149
|
+
|
|
150
|
+
if value is None:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
setattr(self.rtsnode, alua_attr, value)
|
|
154
|
+
|
|
155
|
+
def summary(self):
|
|
156
|
+
return (f"ALUA state: {alua_state_names[self.rtsnode.alua_access_state]}", True)
|
|
157
|
+
|
|
158
|
+
class UIALUATargetPortGroups(UINode):
|
|
159
|
+
'''
|
|
160
|
+
ALUA Target Port Group UI
|
|
161
|
+
'''
|
|
162
|
+
def __init__(self, parent):
|
|
163
|
+
super().__init__("alua", parent)
|
|
164
|
+
self.refresh()
|
|
165
|
+
|
|
166
|
+
def summary(self):
|
|
167
|
+
return (f"ALUA Groups: {len(self.children)}", None)
|
|
168
|
+
|
|
169
|
+
def refresh(self):
|
|
170
|
+
self._children = set()
|
|
171
|
+
|
|
172
|
+
so = self.parent.rtsnode
|
|
173
|
+
for tpg in so.alua_tpgs:
|
|
174
|
+
UIALUATargetPortGroup(tpg, self)
|
|
175
|
+
|
|
176
|
+
def ui_command_create(self, name, tag):
|
|
177
|
+
'''
|
|
178
|
+
Create a new ALUA Target Port Group attached to a storage object.
|
|
179
|
+
'''
|
|
180
|
+
self.assert_root()
|
|
181
|
+
|
|
182
|
+
so = self.parent.rtsnode
|
|
183
|
+
alua_tpg_object = ALUATargetPortGroup(so, name, int(tag))
|
|
184
|
+
self.shell.log.info(f"Created ALUA TPG {alua_tpg_object.name}.")
|
|
185
|
+
ui_alua_tpg = UIALUATargetPortGroup(alua_tpg_object, self)
|
|
186
|
+
return self.new_node(ui_alua_tpg)
|
|
187
|
+
|
|
188
|
+
def ui_command_delete(self, name):
|
|
189
|
+
'''
|
|
190
|
+
Delete the ALUA Target Por Group and unmap it from a LUN if needed.
|
|
191
|
+
'''
|
|
192
|
+
self.assert_root()
|
|
193
|
+
|
|
194
|
+
so = self.parent.rtsnode
|
|
195
|
+
try:
|
|
196
|
+
alua_tpg_object = ALUATargetPortGroup(so, name)
|
|
197
|
+
except:
|
|
198
|
+
raise RTSLibError("Invalid ALUA group name")
|
|
199
|
+
|
|
200
|
+
alua_tpg_object.delete()
|
|
201
|
+
self.refresh()
|
|
202
|
+
|
|
203
|
+
def ui_complete_delete(self, parameters, text, current_param):
|
|
204
|
+
'''
|
|
205
|
+
Parameter auto-completion method for user command delete.
|
|
206
|
+
@param parameters: Parameters on the command line.
|
|
207
|
+
@type parameters: dict
|
|
208
|
+
@param text: Current text of parameter being typed by the user.
|
|
209
|
+
@type text: str
|
|
210
|
+
@param current_param: Name of parameter to complete.
|
|
211
|
+
@type current_param: str
|
|
212
|
+
@return: Possible completions
|
|
213
|
+
@rtype: list of str
|
|
214
|
+
'''
|
|
215
|
+
if current_param == 'name':
|
|
216
|
+
so = self.parent.rtsnode
|
|
217
|
+
|
|
218
|
+
tpgs = [tpg.name for tpg in so.alua_tpgs]
|
|
219
|
+
completions = [tpg for tpg in tpgs if tpg.startswith(text)]
|
|
220
|
+
else:
|
|
221
|
+
completions = []
|
|
222
|
+
|
|
223
|
+
if len(completions) == 1:
|
|
224
|
+
return [completions[0] + ' ']
|
|
225
|
+
return completions
|
|
226
|
+
|
|
227
|
+
class UIBackstores(UINode):
|
|
228
|
+
'''
|
|
229
|
+
The backstores container UI.
|
|
230
|
+
'''
|
|
231
|
+
def __init__(self, parent):
|
|
232
|
+
UINode.__init__(self, 'backstores', parent)
|
|
233
|
+
self.refresh()
|
|
234
|
+
|
|
235
|
+
def _user_backstores(self):
|
|
236
|
+
'''
|
|
237
|
+
tcmu-runner (or other daemon providing the same service) exposes a
|
|
238
|
+
DBus ObjectManager-based iface to find handlers it supports.
|
|
239
|
+
'''
|
|
240
|
+
bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
|
|
241
|
+
try:
|
|
242
|
+
mgr_iface = Gio.DBusProxy.new_sync(bus,
|
|
243
|
+
Gio.DBusProxyFlags.NONE,
|
|
244
|
+
None,
|
|
245
|
+
'org.kernel.TCMUService1',
|
|
246
|
+
'/org/kernel/TCMUService1',
|
|
247
|
+
'org.freedesktop.DBus.ObjectManager',
|
|
248
|
+
None)
|
|
249
|
+
|
|
250
|
+
for k, v in mgr_iface.GetManagedObjects().items():
|
|
251
|
+
tcmu_iface = Gio.DBusProxy.new_sync(bus,
|
|
252
|
+
Gio.DBusProxyFlags.NONE,
|
|
253
|
+
None,
|
|
254
|
+
'org.kernel.TCMUService1',
|
|
255
|
+
k,
|
|
256
|
+
'org.kernel.TCMUService1',
|
|
257
|
+
None)
|
|
258
|
+
yield (k[k.rfind("/") + 1:], tcmu_iface, v)
|
|
259
|
+
except Exception:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
def refresh(self):
|
|
263
|
+
self._children = set()
|
|
264
|
+
UIPSCSIBackstore(self)
|
|
265
|
+
UIRDMCPBackstore(self)
|
|
266
|
+
UIFileIOBackstore(self)
|
|
267
|
+
UIBlockBackstore(self)
|
|
268
|
+
|
|
269
|
+
for name, iface, prop_dict in self._user_backstores():
|
|
270
|
+
UIUserBackedBackstore(self, name, iface, prop_dict)
|
|
271
|
+
|
|
272
|
+
class UIBackstore(UINode):
|
|
273
|
+
'''
|
|
274
|
+
A backstore UI.
|
|
275
|
+
Abstract Base Class, do not instantiate.
|
|
276
|
+
'''
|
|
277
|
+
def __init__(self, plugin, parent):
|
|
278
|
+
UINode.__init__(self, plugin, parent)
|
|
279
|
+
self.refresh()
|
|
280
|
+
|
|
281
|
+
def refresh(self):
|
|
282
|
+
self._children = set()
|
|
283
|
+
for so in RTSRoot().storage_objects:
|
|
284
|
+
if so.plugin == self.name:
|
|
285
|
+
self.so_cls(so, self)
|
|
286
|
+
|
|
287
|
+
def summary(self):
|
|
288
|
+
return (f"Storage Objects: {len(self._children)}", None)
|
|
289
|
+
|
|
290
|
+
def ui_command_delete(self, name, save=None):
|
|
291
|
+
'''
|
|
292
|
+
Recursively deletes the storage object having the specified name. If
|
|
293
|
+
there are LUNs using this storage object, they will be deleted too.
|
|
294
|
+
|
|
295
|
+
EXAMPLE
|
|
296
|
+
=======
|
|
297
|
+
delete mystorage
|
|
298
|
+
----------------
|
|
299
|
+
Deletes the storage object named mystorage, and all associated LUNs.
|
|
300
|
+
'''
|
|
301
|
+
self.assert_root()
|
|
302
|
+
try:
|
|
303
|
+
child = self.get_child(name)
|
|
304
|
+
except ValueError:
|
|
305
|
+
raise ExecutionError(f"No storage object named {name}.")
|
|
306
|
+
|
|
307
|
+
save = self.ui_eval_param(save, 'bool', False)
|
|
308
|
+
if save:
|
|
309
|
+
rn = self.get_root()
|
|
310
|
+
rn._save_backups(default_save_file)
|
|
311
|
+
|
|
312
|
+
child.rtsnode.delete(save=save)
|
|
313
|
+
self.remove_child(child)
|
|
314
|
+
self.shell.log.info(f"Deleted storage object {name}.")
|
|
315
|
+
|
|
316
|
+
def ui_complete_delete(self, parameters, text, current_param):
|
|
317
|
+
'''
|
|
318
|
+
Parameter auto-completion method for user command delete.
|
|
319
|
+
@param parameters: Parameters on the command line.
|
|
320
|
+
@type parameters: dict
|
|
321
|
+
@param text: Current text of parameter being typed by the user.
|
|
322
|
+
@type text: str
|
|
323
|
+
@param current_param: Name of parameter to complete.
|
|
324
|
+
@type current_param: str
|
|
325
|
+
@return: Possible completions
|
|
326
|
+
@rtype: list of str
|
|
327
|
+
'''
|
|
328
|
+
if current_param == 'name':
|
|
329
|
+
names = [child.name for child in self.children]
|
|
330
|
+
completions = [name for name in names
|
|
331
|
+
if name.startswith(text)]
|
|
332
|
+
else:
|
|
333
|
+
completions = []
|
|
334
|
+
|
|
335
|
+
if len(completions) == 1:
|
|
336
|
+
return [completions[0] + ' ']
|
|
337
|
+
return completions
|
|
338
|
+
|
|
339
|
+
def setup_model_alias(self, storageobject):
|
|
340
|
+
if self.shell.prefs['export_backstore_name_as_model']:
|
|
341
|
+
try:
|
|
342
|
+
storageobject.set_attribute("emulate_model_alias", 1)
|
|
343
|
+
except RTSLibError:
|
|
344
|
+
raise ExecutionError("'export_backstore_name_as_model' is set but"
|
|
345
|
+
" emulate_model_alias\n not supported by kernel.")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class UIPSCSIBackstore(UIBackstore):
|
|
349
|
+
'''
|
|
350
|
+
PSCSI backstore UI.
|
|
351
|
+
'''
|
|
352
|
+
def __init__(self, parent):
|
|
353
|
+
self.so_cls = UIPSCSIStorageObject
|
|
354
|
+
UIBackstore.__init__(self, 'pscsi', parent)
|
|
355
|
+
|
|
356
|
+
def ui_command_create(self, name, dev):
|
|
357
|
+
'''
|
|
358
|
+
Creates a PSCSI storage object, with supplied name and SCSI device. The
|
|
359
|
+
SCSI device "dev" can either be a path name to the device, in which
|
|
360
|
+
case it is recommended to use the /dev/disk/by-id hierarchy to have
|
|
361
|
+
consistent naming should your physical SCSI system be modified, or an
|
|
362
|
+
SCSI device ID in the H:C:T:L format, which is not recommended as SCSI
|
|
363
|
+
IDs may vary in time.
|
|
364
|
+
'''
|
|
365
|
+
self.assert_root()
|
|
366
|
+
|
|
367
|
+
if get_block_type(dev) is not None:
|
|
368
|
+
self.shell.log.info("Note: block backstore recommended for "
|
|
369
|
+
"SCSI block devices")
|
|
370
|
+
|
|
371
|
+
so = PSCSIStorageObject(name, dev)
|
|
372
|
+
ui_so = UIPSCSIStorageObject(so, self)
|
|
373
|
+
self.shell.log.info(f"Created pscsi storage object {name} using {dev}")
|
|
374
|
+
return self.new_node(ui_so)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class UIRDMCPBackstore(UIBackstore):
|
|
378
|
+
'''
|
|
379
|
+
RDMCP backstore UI.
|
|
380
|
+
'''
|
|
381
|
+
def __init__(self, parent):
|
|
382
|
+
self.so_cls = UIRamdiskStorageObject
|
|
383
|
+
UIBackstore.__init__(self, 'ramdisk', parent)
|
|
384
|
+
|
|
385
|
+
def ui_command_create(self, name, size, nullio=None, wwn=None):
|
|
386
|
+
'''
|
|
387
|
+
Creates an RDMCP storage object. "size" is the size of the ramdisk.
|
|
388
|
+
|
|
389
|
+
SIZE SYNTAX
|
|
390
|
+
===========
|
|
391
|
+
- If size is an int, it represents a number of bytes.
|
|
392
|
+
- If size is a string, the following units can be used:
|
|
393
|
+
- B or no unit present for bytes
|
|
394
|
+
- k, K, kB, KB for kB (kilobytes)
|
|
395
|
+
- m, M, mB, MB for MB (megabytes)
|
|
396
|
+
- g, G, gB, GB for GB (gigabytes)
|
|
397
|
+
- t, T, tB, TB for TB (terabytes)
|
|
398
|
+
'''
|
|
399
|
+
self.assert_root()
|
|
400
|
+
|
|
401
|
+
nullio = self.ui_eval_param(nullio, 'bool', False)
|
|
402
|
+
wwn = self.ui_eval_param(wwn, 'string', None)
|
|
403
|
+
|
|
404
|
+
so = RDMCPStorageObject(name, human_to_bytes(size), nullio=nullio, wwn=wwn)
|
|
405
|
+
ui_so = UIRamdiskStorageObject(so, self)
|
|
406
|
+
self.setup_model_alias(so)
|
|
407
|
+
self.shell.log.info(f"Created ramdisk {name} with size {size}.")
|
|
408
|
+
return self.new_node(ui_so)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class UIFileIOBackstore(UIBackstore):
|
|
412
|
+
'''
|
|
413
|
+
FileIO backstore UI.
|
|
414
|
+
'''
|
|
415
|
+
def __init__(self, parent):
|
|
416
|
+
self.so_cls = UIFileioStorageObject
|
|
417
|
+
UIBackstore.__init__(self, 'fileio', parent)
|
|
418
|
+
|
|
419
|
+
def _create_file(self, filename, size, sparse=True):
|
|
420
|
+
try:
|
|
421
|
+
f = open(filename, "w+") # noqa: SIM115
|
|
422
|
+
except OSError:
|
|
423
|
+
raise ExecutionError(f"Could not open {filename}")
|
|
424
|
+
try:
|
|
425
|
+
if sparse:
|
|
426
|
+
os.ftruncate(f.fileno(), size)
|
|
427
|
+
else:
|
|
428
|
+
self.shell.log.info("Writing %d bytes" % size)
|
|
429
|
+
try:
|
|
430
|
+
# Prior to version 3.3, Python does not provide fallocate
|
|
431
|
+
os.posix_fallocate(f.fileno(), 0, size)
|
|
432
|
+
except AttributeError:
|
|
433
|
+
while size > 0:
|
|
434
|
+
write_size = min(size, 1024)
|
|
435
|
+
f.write("\0" * write_size)
|
|
436
|
+
size -= write_size
|
|
437
|
+
except OSError:
|
|
438
|
+
Path(filename).unlink()
|
|
439
|
+
raise ExecutionError("Could not expand file to %d bytes" % size)
|
|
440
|
+
except OverflowError:
|
|
441
|
+
raise ExecutionError("The file size is too large (%d bytes)" % size)
|
|
442
|
+
finally:
|
|
443
|
+
f.close()
|
|
444
|
+
|
|
445
|
+
def ui_command_create(self, name, file_or_dev, size=None, write_back=None,
|
|
446
|
+
sparse=None, wwn=None):
|
|
447
|
+
'''
|
|
448
|
+
Creates a FileIO storage object. If "file_or_dev" is a path
|
|
449
|
+
to a regular file to be used as backend, then the "size"
|
|
450
|
+
parameter is mandatory. Else, if "file_or_dev" is a path to a
|
|
451
|
+
block device, the size parameter must be omitted. If
|
|
452
|
+
present, "size" is the size of the file to be used, "file"
|
|
453
|
+
the path to the file or "dev" the path to a block device. The
|
|
454
|
+
"write_back" parameter is a boolean controlling write
|
|
455
|
+
caching. It is enabled by default. The "sparse" parameter is
|
|
456
|
+
only applicable when creating a new backing file. It is a
|
|
457
|
+
boolean stating if the created file should be created as a
|
|
458
|
+
sparse file (the default), or fully initialized.
|
|
459
|
+
|
|
460
|
+
SIZE SYNTAX
|
|
461
|
+
===========
|
|
462
|
+
- If size is an int, it represents a number of bytes.
|
|
463
|
+
- If size is a string, the following units can be used:
|
|
464
|
+
- B or no unit present for bytes
|
|
465
|
+
- k, K, kB, KB for kB (kilobytes)
|
|
466
|
+
- m, M, mB, MB for MB (megabytes)
|
|
467
|
+
- g, G, gB, GB for GB (gigabytes)
|
|
468
|
+
- t, T, tB, TB for TB (terabytes)
|
|
469
|
+
'''
|
|
470
|
+
self.assert_root()
|
|
471
|
+
|
|
472
|
+
sparse = self.ui_eval_param(sparse, 'bool', True)
|
|
473
|
+
write_back = self.ui_eval_param(write_back, 'bool', True)
|
|
474
|
+
wwn = self.ui_eval_param(wwn, 'string', None)
|
|
475
|
+
|
|
476
|
+
self.shell.log.debug(f"Using params size={size} write_back={write_back} sparse={sparse}")
|
|
477
|
+
|
|
478
|
+
file_or_dev = os.path.expanduser(file_or_dev)
|
|
479
|
+
# can't use is_dev_in_use() on files so just check against other
|
|
480
|
+
# storage object paths
|
|
481
|
+
file_or_dev_path = Path(file_or_dev)
|
|
482
|
+
if file_or_dev_path.exists():
|
|
483
|
+
for so in RTSRoot().storage_objects:
|
|
484
|
+
if so.udev_path and file_or_dev_path.samefile(so.udev_path):
|
|
485
|
+
raise ExecutionError(f"storage object for {file_or_dev} already exists: {so.name}")
|
|
486
|
+
|
|
487
|
+
if get_block_type(file_or_dev) is not None:
|
|
488
|
+
if size:
|
|
489
|
+
self.shell.log.info("Block device, size parameter ignored")
|
|
490
|
+
size = None
|
|
491
|
+
self.shell.log.info("Note: block backstore preferred for best results")
|
|
492
|
+
elif Path(file_or_dev).is_file():
|
|
493
|
+
new_size = os.path.getsize(file_or_dev)
|
|
494
|
+
if size:
|
|
495
|
+
self.shell.log.info(f"{file_or_dev} exists, using its size ({new_size} bytes) instead")
|
|
496
|
+
size = new_size
|
|
497
|
+
elif Path(file_or_dev).exists():
|
|
498
|
+
raise ExecutionError(f"Path {file_or_dev} exists but is not a file")
|
|
499
|
+
else:
|
|
500
|
+
# create file and extend to given file size
|
|
501
|
+
if not size:
|
|
502
|
+
raise ExecutionError("Attempting to create file for new fileio backstore, need a size")
|
|
503
|
+
size = human_to_bytes(size)
|
|
504
|
+
self._create_file(file_or_dev, size, sparse)
|
|
505
|
+
|
|
506
|
+
so = FileIOStorageObject(name, file_or_dev, size,
|
|
507
|
+
write_back=write_back, wwn=wwn)
|
|
508
|
+
ui_so = UIFileioStorageObject(so, self)
|
|
509
|
+
self.setup_model_alias(so)
|
|
510
|
+
self.shell.log.info(f"Created fileio {name} with size {so.size}")
|
|
511
|
+
return self.new_node(ui_so)
|
|
512
|
+
|
|
513
|
+
def ui_complete_create(self, parameters, text, current_param):
|
|
514
|
+
'''
|
|
515
|
+
Auto-completes the file name
|
|
516
|
+
'''
|
|
517
|
+
if current_param != 'file_or_dev':
|
|
518
|
+
return []
|
|
519
|
+
completions = complete_path(text, lambda x: stat.S_ISREG(x) or stat.S_ISBLK(x))
|
|
520
|
+
if len(completions) == 1 and not completions[0].endswith('/'):
|
|
521
|
+
completions = [completions[0] + ' ']
|
|
522
|
+
return completions
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class UIBlockBackstore(UIBackstore):
|
|
526
|
+
'''
|
|
527
|
+
Block backstore UI.
|
|
528
|
+
'''
|
|
529
|
+
def __init__(self, parent):
|
|
530
|
+
self.so_cls = UIBlockStorageObject
|
|
531
|
+
UIBackstore.__init__(self, 'block', parent)
|
|
532
|
+
|
|
533
|
+
def _ui_block_ro_check(self, dev):
|
|
534
|
+
BLKROGET = 0x0000125E # noqa: N806
|
|
535
|
+
try:
|
|
536
|
+
f = os.open(dev, os.O_RDONLY)
|
|
537
|
+
except OSError:
|
|
538
|
+
raise ExecutionError(f"Could not open {dev}")
|
|
539
|
+
# ioctl returns an int. Provision a buffer for it
|
|
540
|
+
buf = array.array('b', [0] * 4)
|
|
541
|
+
try:
|
|
542
|
+
fcntl.ioctl(f, BLKROGET, buf)
|
|
543
|
+
except OSError:
|
|
544
|
+
os.close(f)
|
|
545
|
+
return False
|
|
546
|
+
|
|
547
|
+
os.close(f)
|
|
548
|
+
if struct.unpack('I', buf)[0] == 0:
|
|
549
|
+
return False
|
|
550
|
+
return True
|
|
551
|
+
|
|
552
|
+
def ui_command_create(self, name, dev, readonly=None, wwn=None):
|
|
553
|
+
'''
|
|
554
|
+
Creates an Block Storage object. "dev" is the path to the TYPE_DISK
|
|
555
|
+
block device to use.
|
|
556
|
+
'''
|
|
557
|
+
self.assert_root()
|
|
558
|
+
|
|
559
|
+
ro_string = self.ui_eval_param(readonly, 'string', None)
|
|
560
|
+
readonly = self._ui_block_ro_check(dev) if ro_string is None else self.ui_eval_param(readonly, "bool", False)
|
|
561
|
+
|
|
562
|
+
wwn = self.ui_eval_param(wwn, 'string', None)
|
|
563
|
+
|
|
564
|
+
so = BlockStorageObject(name, dev, readonly=readonly, wwn=wwn)
|
|
565
|
+
ui_so = UIBlockStorageObject(so, self)
|
|
566
|
+
self.setup_model_alias(so)
|
|
567
|
+
self.shell.log.info(f"Created block storage object {name} using {dev}.")
|
|
568
|
+
return self.new_node(ui_so)
|
|
569
|
+
|
|
570
|
+
def ui_complete_create(self, parameters, text, current_param):
|
|
571
|
+
'''
|
|
572
|
+
Auto-completes the device name
|
|
573
|
+
'''
|
|
574
|
+
if current_param != 'dev':
|
|
575
|
+
return []
|
|
576
|
+
completions = complete_path(text, stat.S_ISBLK)
|
|
577
|
+
if len(completions) == 1 and not completions[0].endswith('/'):
|
|
578
|
+
completions = [completions[0] + ' ']
|
|
579
|
+
return completions
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class UIUserBackedBackstore(UIBackstore):
|
|
583
|
+
'''
|
|
584
|
+
User backstore UI.
|
|
585
|
+
'''
|
|
586
|
+
def __init__(self, parent, name, iface, prop_dict):
|
|
587
|
+
self.so_cls = UIUserBackedStorageObject
|
|
588
|
+
self.handler = name
|
|
589
|
+
self.iface = iface
|
|
590
|
+
self.prop_dict = prop_dict
|
|
591
|
+
super().__init__("user:" + name, parent)
|
|
592
|
+
|
|
593
|
+
def refresh(self):
|
|
594
|
+
self._children = set()
|
|
595
|
+
for so in RTSRoot().storage_objects:
|
|
596
|
+
if so.plugin == 'user' and so.config:
|
|
597
|
+
idx = so.config.find("/")
|
|
598
|
+
handler = so.config[:idx]
|
|
599
|
+
if handler == self.handler:
|
|
600
|
+
self.so_cls(so, self)
|
|
601
|
+
|
|
602
|
+
def ui_command_help(self, topic=None):
|
|
603
|
+
super().ui_command_help(topic)
|
|
604
|
+
if topic == "create":
|
|
605
|
+
print("CFGSTRING FORMAT")
|
|
606
|
+
print("=================")
|
|
607
|
+
x = self.prop_dict.get("org.kernel.TCMUService1", {})
|
|
608
|
+
print(x.get("ConfigDesc", "No description."))
|
|
609
|
+
print()
|
|
610
|
+
|
|
611
|
+
def ui_command_create(self, name, size, cfgstring, wwn=None,
|
|
612
|
+
hw_max_sectors=None, control=None):
|
|
613
|
+
'''
|
|
614
|
+
Creates a User-backed storage object.
|
|
615
|
+
|
|
616
|
+
SIZE SYNTAX
|
|
617
|
+
===========
|
|
618
|
+
- If size is an int, it represents a number of bytes.
|
|
619
|
+
- If size is a string, the following units can be used:
|
|
620
|
+
- B or no unit present for bytes
|
|
621
|
+
- k, K, kB, KB for kB (kilobytes)
|
|
622
|
+
- m, M, mB, MB for MB (megabytes)
|
|
623
|
+
- g, G, gB, GB for GB (gigabytes)
|
|
624
|
+
- t, T, tB, TB for TB (terabytes)
|
|
625
|
+
'''
|
|
626
|
+
|
|
627
|
+
size = human_to_bytes(size)
|
|
628
|
+
wwn = self.ui_eval_param(wwn, 'string', None)
|
|
629
|
+
|
|
630
|
+
config = self.handler + "/" + cfgstring
|
|
631
|
+
|
|
632
|
+
ok, errmsg = self.iface.CheckConfig('(s)', config)
|
|
633
|
+
if not ok:
|
|
634
|
+
raise ExecutionError(f"cfgstring invalid: {errmsg}")
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
so = UserBackedStorageObject(name, size=size, config=config,
|
|
638
|
+
wwn=wwn, hw_max_sectors=hw_max_sectors,
|
|
639
|
+
control=control)
|
|
640
|
+
except:
|
|
641
|
+
raise ExecutionError("UserBackedStorageObject creation failed.")
|
|
642
|
+
|
|
643
|
+
ui_so = UIUserBackedStorageObject(so, self)
|
|
644
|
+
self.shell.log.info("Created user-backed storage object %s size %d."
|
|
645
|
+
% (name, size))
|
|
646
|
+
return self.new_node(ui_so)
|
|
647
|
+
|
|
648
|
+
def ui_command_changemedium(self, name, size, cfgstring):
|
|
649
|
+
size = human_to_bytes(size)
|
|
650
|
+
config = self.handler + "/" + cfgstring
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
rc, errmsg = self.iface.ChangeMedium('(sts)', name, size, config)
|
|
654
|
+
except Exception as e:
|
|
655
|
+
raise ExecutionError(f"ChangeMedium failed: {e}")
|
|
656
|
+
else:
|
|
657
|
+
if rc == 0:
|
|
658
|
+
self.shell.log.info("Medium Changed.")
|
|
659
|
+
else:
|
|
660
|
+
raise ExecutionError(f"ChangeMedium failed: {errmsg}")
|
|
661
|
+
|
|
662
|
+
class UIStorageObject(UIRTSLibNode):
|
|
663
|
+
'''
|
|
664
|
+
A storage object UI.
|
|
665
|
+
Abstract Base Class, do not instantiate.
|
|
666
|
+
'''
|
|
667
|
+
ui_desc_attributes = {
|
|
668
|
+
'block_size': ('number', 'Block size of the underlying device.'),
|
|
669
|
+
'emulate_3pc': ('number', 'If set to 1, enable Third Party Copy.'),
|
|
670
|
+
'emulate_caw': ('number', 'If set to 1, enable Compare and Write.'),
|
|
671
|
+
'emulate_dpo': ('number', 'If set to 1, turn on Disable Page Out.'),
|
|
672
|
+
'emulate_fua_read': ('number', 'If set to 1, enable Force Unit Access read.'),
|
|
673
|
+
'emulate_fua_write': ('number', 'If set to 1, enable Force Unit Access write.'),
|
|
674
|
+
'emulate_model_alias': ('number', 'If set to 1, use the backend device name for the model alias.'),
|
|
675
|
+
'emulate_rest_reord': ('number', 'If set to 0, the Queue Algorithm Modifier is Restricted Reordering.'),
|
|
676
|
+
'emulate_tas': ('number', 'If set to 1, enable Task Aborted Status.'),
|
|
677
|
+
'emulate_tpu': ('number', 'If set to 1, enable Thin Provisioning Unmap.'),
|
|
678
|
+
'emulate_tpws': ('number', 'If set to 1, enable Thin Provisioning Write Same.'),
|
|
679
|
+
'emulate_ua_intlck_ctrl': ('number', 'If set to 1, enable Unit Attention Interlock.'),
|
|
680
|
+
'emulate_write_cache': ('number', 'If set to 1, turn on Write Cache Enable.'),
|
|
681
|
+
'emulate_pr': ('number', 'If set to 1, enable SCSI Reservations.'),
|
|
682
|
+
'enforce_pr_isids': ('number', 'If set to 1, enforce persistent reservation ISIDs.'),
|
|
683
|
+
'force_pr_aptpl': ('number',
|
|
684
|
+
'If set to 1, force SPC-3 PR Activate Persistence across Target Power Loss operation.'),
|
|
685
|
+
'fabric_max_sectors': ('number', 'Maximum number of sectors the fabric can transfer at once.'),
|
|
686
|
+
'hw_block_size': ('number', 'Hardware block size in bytes.'),
|
|
687
|
+
'hw_max_sectors': ('number', 'Maximum number of sectors the hardware can transfer at once.'),
|
|
688
|
+
'control': ('string',
|
|
689
|
+
'Comma separated string of control=value tuples that will be passed to kernel control file.'),
|
|
690
|
+
'hw_pi_prot_type': ('number', 'If non-zero, DIF protection is enabled on the underlying hardware.'),
|
|
691
|
+
'hw_queue_depth': ('number', 'Hardware queue depth.'),
|
|
692
|
+
'is_nonrot': ('number', 'If set to 1, the backstore is a non rotational device.'),
|
|
693
|
+
'max_unmap_block_desc_count': ('number', 'Maximum number of block descriptors for UNMAP.'),
|
|
694
|
+
'max_unmap_lba_count': ('number', 'Maximum number of LBA for UNMAP.'),
|
|
695
|
+
'max_write_same_len': ('number', 'Maximum length for WRITE_SAME.'),
|
|
696
|
+
'optimal_sectors': ('number', 'Optimal request size in sectors.'),
|
|
697
|
+
'pi_prot_format': ('number', 'DIF protection format.'),
|
|
698
|
+
'pi_prot_type': ('number', 'DIF protection type.'),
|
|
699
|
+
'queue_depth': ('number', 'Queue depth.'),
|
|
700
|
+
'unmap_granularity': ('number', 'UNMAP granularity.'),
|
|
701
|
+
'unmap_granularity_alignment': ('number', 'UNMAP granularity alignment.'),
|
|
702
|
+
'unmap_zeroes_data': ('number', 'If set to 1, zeroes are read back after an UNMAP.'),
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
def __init__(self, storage_object, parent):
|
|
706
|
+
name = storage_object.name
|
|
707
|
+
UIRTSLibNode.__init__(self, name, storage_object, parent)
|
|
708
|
+
self.refresh()
|
|
709
|
+
|
|
710
|
+
UIALUATargetPortGroups(self)
|
|
711
|
+
|
|
712
|
+
def ui_command_version(self):
|
|
713
|
+
'''
|
|
714
|
+
Displays the version of the current backstore's plugin.
|
|
715
|
+
'''
|
|
716
|
+
self.shell.con.display(f"Backstore plugin {self.rtsnode.plugin} {self.rtsnode.version}")
|
|
717
|
+
|
|
718
|
+
def ui_command_saveconfig(self, savefile=None):
|
|
719
|
+
'''
|
|
720
|
+
Save configuration of this StorageObject.
|
|
721
|
+
'''
|
|
722
|
+
so = self.rtsnode
|
|
723
|
+
rn = self.get_root()
|
|
724
|
+
|
|
725
|
+
if not savefile:
|
|
726
|
+
savefile = default_save_file
|
|
727
|
+
|
|
728
|
+
savefile = os.path.expanduser(savefile)
|
|
729
|
+
|
|
730
|
+
rn._save_backups(savefile)
|
|
731
|
+
|
|
732
|
+
rn.rtsroot.save_to_file(savefile,
|
|
733
|
+
'/backstores/' + so.plugin + '/' + so.name)
|
|
734
|
+
|
|
735
|
+
self.shell.log.info(f"Storage Object '{so.plugin}:{so.name}' config saved to {savefile}.")
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class UIPSCSIStorageObject(UIStorageObject):
|
|
739
|
+
def summary(self):
|
|
740
|
+
so = self.rtsnode
|
|
741
|
+
return (f"{so.udev_path} {so.status}", True)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
class UIRamdiskStorageObject(UIStorageObject):
|
|
745
|
+
def summary(self):
|
|
746
|
+
so = self.rtsnode
|
|
747
|
+
|
|
748
|
+
nullio_str = ""
|
|
749
|
+
if so.nullio:
|
|
750
|
+
nullio_str = "nullio "
|
|
751
|
+
|
|
752
|
+
return (f"{nullio_str}({bytes_to_human(so.size)}) {so.status}", True)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
class UIFileioStorageObject(UIStorageObject):
|
|
756
|
+
def summary(self):
|
|
757
|
+
so = self.rtsnode
|
|
758
|
+
|
|
759
|
+
wb_str = "write-back" if so.write_back else "write-thru"
|
|
760
|
+
|
|
761
|
+
return (f"{so.udev_path} ({bytes_to_human(so.size)}) {wb_str} {so.status}", True)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class UIBlockStorageObject(UIStorageObject):
|
|
765
|
+
def summary(self):
|
|
766
|
+
so = self.rtsnode
|
|
767
|
+
|
|
768
|
+
wb_str = "write-back" if so.write_back else "write-thru"
|
|
769
|
+
|
|
770
|
+
ro_str = ""
|
|
771
|
+
if so.readonly:
|
|
772
|
+
ro_str = "ro "
|
|
773
|
+
|
|
774
|
+
return f"{so.udev_path} ({bytes_to_human(so.size)}) {ro_str}{wb_str} {so.status}", True
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
class UIUserBackedStorageObject(UIStorageObject):
|
|
778
|
+
def summary(self):
|
|
779
|
+
so = self.rtsnode
|
|
780
|
+
|
|
781
|
+
if not so.config:
|
|
782
|
+
config_str = "(no config)"
|
|
783
|
+
else:
|
|
784
|
+
idx = so.config.find("/")
|
|
785
|
+
config_str = so.config[idx + 1:]
|
|
786
|
+
|
|
787
|
+
return (f"{config_str} ({bytes_to_human(so.size)}) {so.status}", True)
|