targetcli 3.0.0.dev0__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.
@@ -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)