fb-vmware 1.7.1__py3-none-any.whl → 1.8.1__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.
Files changed (36) hide show
  1. fb_vmware/__init__.py +1 -1
  2. fb_vmware/app/__init__.py +285 -6
  3. fb_vmware/app/get_host_list.py +115 -100
  4. fb_vmware/app/get_network_list.py +176 -218
  5. fb_vmware/app/get_rpool_list.py +386 -0
  6. fb_vmware/app/get_storage_cluster_info.py +303 -0
  7. fb_vmware/app/get_storage_cluster_list.py +100 -107
  8. fb_vmware/app/get_storage_list.py +145 -112
  9. fb_vmware/app/get_vm_info.py +79 -17
  10. fb_vmware/app/get_vm_list.py +169 -95
  11. fb_vmware/app/search_storage.py +470 -0
  12. fb_vmware/argparse_actions.py +78 -0
  13. fb_vmware/base.py +28 -1
  14. fb_vmware/cluster.py +99 -7
  15. fb_vmware/connect.py +450 -20
  16. fb_vmware/datastore.py +195 -6
  17. fb_vmware/dc.py +19 -1
  18. fb_vmware/ds_cluster.py +215 -2
  19. fb_vmware/dvs.py +37 -1
  20. fb_vmware/errors.py +31 -10
  21. fb_vmware/host.py +40 -2
  22. fb_vmware/host_port_group.py +1 -2
  23. fb_vmware/network.py +17 -1
  24. fb_vmware/obj.py +30 -1
  25. fb_vmware/vm.py +19 -1
  26. fb_vmware/xlate.py +8 -13
  27. fb_vmware-1.8.1.data/data/share/locale/de/LC_MESSAGES/fb_vmware.mo +0 -0
  28. fb_vmware-1.8.1.data/data/share/locale/en/LC_MESSAGES/fb_vmware.mo +0 -0
  29. {fb_vmware-1.7.1.dist-info → fb_vmware-1.8.1.dist-info}/METADATA +2 -1
  30. fb_vmware-1.8.1.dist-info/RECORD +40 -0
  31. {fb_vmware-1.7.1.dist-info → fb_vmware-1.8.1.dist-info}/entry_points.txt +3 -0
  32. fb_vmware-1.7.1.data/data/share/locale/de_DE/LC_MESSAGES/fb_vmware.mo +0 -0
  33. fb_vmware-1.7.1.data/data/share/locale/en_US/LC_MESSAGES/fb_vmware.mo +0 -0
  34. fb_vmware-1.7.1.dist-info/RECORD +0 -36
  35. {fb_vmware-1.7.1.dist-info → fb_vmware-1.8.1.dist-info}/WHEEL +0 -0
  36. {fb_vmware-1.7.1.dist-info → fb_vmware-1.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ @summary: Search a datastore for vdisk of a given size in a VMware vSphere.
5
+
6
+ @author: Frank Brehm
7
+ @contact: frank@brehm-online.com
8
+ @copyright: © 2026 by Frank Brehm, Berlin
9
+ """
10
+ from __future__ import absolute_import, print_function
11
+
12
+ # Standard modules
13
+ import locale
14
+ import logging
15
+ import pathlib
16
+ import sys
17
+
18
+ # from fb_tools.argparse_actions import RegexOptionAction
19
+ from fb_tools.common import pp
20
+ from fb_tools.spinner import Spinner
21
+ from fb_tools.xlate import format_list
22
+
23
+ # Own modules
24
+ from . import BaseVmwareApplication
25
+ from .. import __version__ as GLOBAL_VERSION
26
+ from ..argparse_actions import NonNegativeIntegerOptionAction
27
+ from ..datastore import VsphereDatastoreDict
28
+ from ..ds_cluster import VsphereDsCluster
29
+ from ..ds_cluster import VsphereDsClusterDict
30
+ from ..errors import VSphereExpectedError
31
+ from ..errors import VSphereNoDatastoreFoundError
32
+ from ..errors import VSphereNoDsClusterFoundError
33
+ from ..xlate import XLATOR
34
+
35
+ __version__ = "0.6.2"
36
+ LOG = logging.getLogger(__name__)
37
+
38
+ _ = XLATOR.gettext
39
+ ngettext = XLATOR.ngettext
40
+ npgettext = XLATOR.npgettext
41
+ pgettext = XLATOR.pgettext
42
+
43
+
44
+ # =============================================================================
45
+ class SearchStorageApp(BaseVmwareApplication):
46
+ """Class for the application object."""
47
+
48
+ show_simulate_option = False
49
+ default_all_vspheres = False
50
+
51
+ valid_storage_types = []
52
+ for storage_type in VsphereDsCluster.valid_storage_types:
53
+ valid_storage_types.append(storage_type.lower())
54
+ valid_storage_types.append("any")
55
+
56
+ # -------------------------------------------------------------------------
57
+ def __init__(
58
+ self,
59
+ appname=None,
60
+ verbose=0,
61
+ version=GLOBAL_VERSION,
62
+ base_dir=None,
63
+ initialized=False,
64
+ usage=None,
65
+ description=None,
66
+ argparse_epilog=None,
67
+ argparse_prefix_chars="-",
68
+ env_prefix=None,
69
+ ):
70
+ """Initialize a GetStorageListApp object."""
71
+ desc = _(
72
+ "Searches for a storage cluster or a datastore for a planned volume of a given size."
73
+ )
74
+
75
+ self.datastores = VsphereDatastoreDict()
76
+ self.ds_clusters = VsphereDsClusterDict()
77
+
78
+ self.vsphere_name = None
79
+ self.cur_vsphere = None
80
+ self.dc = None
81
+ self.cluster = None
82
+ self.cluster_type = None
83
+
84
+ self.disk_size_gb = None
85
+ self.storage_type = None
86
+
87
+ super(SearchStorageApp, self).__init__(
88
+ appname=appname,
89
+ verbose=verbose,
90
+ version=version,
91
+ base_dir=base_dir,
92
+ description=desc,
93
+ initialized=False,
94
+ )
95
+
96
+ self.initialized = True
97
+
98
+ # -------------------------------------------------------------------------
99
+ def init_arg_parser(self):
100
+ """Public available method to initiate the argument parser."""
101
+ search_options = self.arg_parser.add_argument_group(_("Search options"))
102
+
103
+ search_options.add_argument(
104
+ "-S",
105
+ "--size",
106
+ dest="size",
107
+ type=int,
108
+ metavar=_("GBYTE"),
109
+ action=NonNegativeIntegerOptionAction,
110
+ may_zero=False,
111
+ help=_(
112
+ "The size of the virtual disk, for which a storage location should be searched."
113
+ ),
114
+ )
115
+
116
+ typelist = format_list(self.valid_storage_types, do_repr=True)
117
+ help_msg = _(
118
+ "The required storage type of the resulting volume. Valid types are {types}."
119
+ ).format(types=typelist, deflt="any")
120
+ search_options.add_argument(
121
+ "-T",
122
+ "--type",
123
+ "--storage-type",
124
+ metavar=_("TYPE"),
125
+ choices=self.valid_storage_types,
126
+ help=help_msg,
127
+ )
128
+
129
+ search_options.add_argument(
130
+ "--vs",
131
+ "--vsphere",
132
+ dest="req_vsphere",
133
+ help=_(
134
+ "The vSphere name from configuration, in which the storage should be searched."
135
+ ),
136
+ )
137
+
138
+ search_options.add_argument(
139
+ "-D",
140
+ "--dc",
141
+ "--datacenter",
142
+ metavar=_("DATACENTER"),
143
+ dest="dc",
144
+ help=_("The virtual datacenter in vSphere, in which the storage should be searched."),
145
+ )
146
+
147
+ search_options.add_argument(
148
+ "--cluster",
149
+ metavar=_("CLUSTER"),
150
+ dest="cluster",
151
+ help=_(
152
+ "The computing cluster, which should be connected with the datastore cluster "
153
+ "or datastore in result."
154
+ ),
155
+ )
156
+
157
+ super(SearchStorageApp, self).init_arg_parser()
158
+
159
+ # -------------------------------------------------------------------------
160
+ def add_vsphere_argument(self):
161
+ """Add a commandline option for selecting the vSphere to use."""
162
+ pass
163
+
164
+ # -------------------------------------------------------------------------
165
+ def perform_arg_parser(self):
166
+ """Evaluate command line parameters."""
167
+ super(SearchStorageApp, self).perform_arg_parser()
168
+
169
+ LOG.debug("Given arguments:\n" + pp(self.args))
170
+
171
+ if getattr(self.args, "size", None) is not None:
172
+ self.disk_size_gb = self.args.size
173
+
174
+ if getattr(self.args, "type", None) is not None:
175
+ self.storage_type = getattr(self.args, "type", None)
176
+
177
+ if self.args.req_vsphere:
178
+ vsphere = self.args.req_vsphere
179
+ self.args.req_vsphere = [vsphere]
180
+ LOG.info(_("Selected vSphere: {}").format(self.colored(vsphere, "CYAN")))
181
+
182
+ if getattr(self.args, "dc", None) is not None and self.args.dc.strip() != "":
183
+ self.dc = self.args.dc.strip()
184
+
185
+ if getattr(self.args, "cluster", None) is not None and self.args.cluster.strip() != "":
186
+ self.cluster = self.args.cluster.strip()
187
+
188
+ # -------------------------------------------------------------------------
189
+ def pre_run(self):
190
+ """Execute some actions before the main routine."""
191
+ if self.disk_size_gb is None:
192
+ self.disk_size_gb = self.prompt_for_disk_size()
193
+
194
+ storage_type = self.select_storage_type(self.storage_type)
195
+ if storage_type is None:
196
+ self.exit(1)
197
+ self.storage_type = storage_type
198
+
199
+ vs_name = self.select_vsphere()
200
+ self.do_vspheres = [vs_name]
201
+
202
+ super(SearchStorageApp, self).pre_run()
203
+ self.vsphere_name = vs_name
204
+ self.cur_vsphere = self.vsphere[vs_name]
205
+
206
+ dc_name = self.select_datacenter(vs_name, self.dc)
207
+ if dc_name is None:
208
+ self.exit(1)
209
+ self.dc = dc_name
210
+
211
+ (cluster_name, cluster_type) = self.select_computing_cluster(
212
+ vs_name=vs_name, dc_name=dc_name, cluster_name=self.cluster
213
+ )
214
+ if cluster_name[0] is None:
215
+ self.exit(1)
216
+ self.cluster = cluster_name
217
+ self.cluster_type = cluster_type
218
+
219
+ LOG.info(
220
+ _(
221
+ "Searching a storage location in vSphere {vs}, virtual datacenter {dc} "
222
+ "connected with the {cl_type} {cl} "
223
+ "for a disk of {sz} of type {st_type}."
224
+ ).format(
225
+ vs=self.colored(vs_name, "CYAN"),
226
+ dc=self.colored(dc_name, "CYAN"),
227
+ cl_type=self.cluster_type,
228
+ cl=self.colored(self.cluster, "CYAN"),
229
+ sz=self.colored(str(self.disk_size_gb) + " GiByte", "CYAN"),
230
+ st_type=self.colored(storage_type, "CYAN"),
231
+ )
232
+ )
233
+
234
+ # -------------------------------------------------------------------------
235
+ def _run(self):
236
+
237
+ LOG.debug(_("Starting {a!r}, version {v!r} ...").format(a=self.appname, v=self.version))
238
+
239
+ ret = 0
240
+ try:
241
+ self.get_storages()
242
+ self.search_for_space()
243
+ finally:
244
+ self.cleaning_up()
245
+
246
+ self.exit(ret)
247
+
248
+ # -------------------------------------------------------------------------
249
+ def get_storages(self):
250
+ """Retrieve all datastore clusters and storages in current vSphere and datacenter."""
251
+ LOG.info(
252
+ _("Collect all datastore clusters and storages in current vSphere and datacenter.")
253
+ )
254
+
255
+ self.get_datastore_clusters()
256
+ len_ds_clusters = len(self.ds_clusters)
257
+ if len_ds_clusters:
258
+ one = pgettext("found_ds_cluster", "one")
259
+ msg = ngettext(
260
+ "Found total {one} datastore cluster in vSphere {vs}, datacenter {dc}.",
261
+ "Found total {nr} datastore clusters in vSphere {vs}, datacenter {dc}.",
262
+ len_ds_clusters,
263
+ )
264
+ msg = msg.format(
265
+ one=self.colored(one, "CYAN"),
266
+ nr=self.colored(str(len_ds_clusters), "CYAN"),
267
+ vs=self.colored(self.vsphere_name, "CYAN"),
268
+ dc=self.colored(self.dc, "CYAN"),
269
+ )
270
+ else:
271
+ msg = _("Did not found a datastore cluster in vSphere {vs}, datacenter {dc}.").format(
272
+ vs=self.colored(self.vsphere_name, "CYAN"),
273
+ dc=self.colored(self.dc, "CYAN"),
274
+ )
275
+ LOG.info(msg)
276
+
277
+ self.get_datastores()
278
+ len_datastores = len(self.datastores)
279
+ if len_datastores:
280
+ one = pgettext("found_datastore", "one")
281
+ msg = ngettext(
282
+ "Found total {one} datastore in vSphere {vs}, datacenter {dc}.",
283
+ "Found total {nr} datastores in vSphere {vs}, datacenter {dc}.",
284
+ len_datastores,
285
+ )
286
+ msg = msg.format(
287
+ one=self.colored(one, "CYAN"),
288
+ nr=self.colored(str(len_datastores), "CYAN"),
289
+ vs=self.colored(self.vsphere_name, "CYAN"),
290
+ dc=self.colored(self.dc, "CYAN"),
291
+ )
292
+ else:
293
+ msg = _("Did not found a datastore in vSphere {vs}, datacenter {dc}.").format(
294
+ vs=self.colored(self.vsphere_name, "CYAN"),
295
+ dc=self.colored(self.dc, "CYAN"),
296
+ )
297
+ LOG.info(msg)
298
+
299
+ if not len_ds_clusters and not len_datastores:
300
+ msg = _(
301
+ "Found neither a datastore cluster nor a datastore in vSphere {vs}, "
302
+ "datacenter {dc}."
303
+ ).format(
304
+ vs=self.colored(self.vsphere_name, "CYAN"),
305
+ dc=self.colored(self.dc, "CYAN"),
306
+ )
307
+ LOG.error(msg)
308
+ self.exit(7)
309
+
310
+ # -------------------------------------------------------------------------
311
+ def get_datastore_clusters(self):
312
+ """Retrieve all datastore clusters in current vSphere and datacenter."""
313
+ self.ds_clusters = VsphereDsClusterDict()
314
+
315
+ # ----------
316
+ def _get_datastore_clusters():
317
+ try:
318
+ self.cur_vsphere.get_ds_clusters(
319
+ vsphere_name=self.vsphere_name,
320
+ search_in_dc=self.dc,
321
+ warn_if_empty=False,
322
+ detailled=True,
323
+ )
324
+ except VSphereExpectedError as e:
325
+ LOG.error(str(e))
326
+ self.exit(6)
327
+
328
+ for cluster_name in self.cur_vsphere.ds_clusters:
329
+ self.ds_clusters.append(self.cur_vsphere.ds_clusters[cluster_name])
330
+
331
+ if self.verbose or self.quiet:
332
+ _get_datastore_clusters()
333
+ else:
334
+ spin_prompt = _("Getting all vSphere storage clusters ...")
335
+ spinner_name = self.get_random_spinner_name()
336
+ with Spinner(spin_prompt, spinner_name):
337
+ _get_datastore_clusters()
338
+ sys.stdout.write(" " * len(spin_prompt))
339
+ sys.stdout.write("\r")
340
+ sys.stdout.flush()
341
+
342
+ if self.verbose > 2:
343
+ LOG.debug(_("Found datastore clusters:") + "\n" + pp(self.ds_clusters.as_list()))
344
+
345
+ # -------------------------------------------------------------------------
346
+ def get_datastores(self):
347
+ """Retrieve datastores in current vSphere and datacenter."""
348
+ self.datastores = VsphereDatastoreDict()
349
+
350
+ # ----------
351
+ def _get_datastores():
352
+ try:
353
+ self.cur_vsphere.get_datastores(
354
+ vsphere_name=self.vsphere_name,
355
+ search_in_dc=self.dc,
356
+ warn_if_empty=False,
357
+ detailled=True,
358
+ no_local_ds=False,
359
+ )
360
+ except VSphereExpectedError as e:
361
+ LOG.error(str(e))
362
+ self.exit(6)
363
+
364
+ for ds_name in self.cur_vsphere.datastores:
365
+ self.datastores.append(self.cur_vsphere.datastores[ds_name])
366
+
367
+ if self.verbose or self.quiet:
368
+ _get_datastores()
369
+ else:
370
+ spin_prompt = _("Getting all vSphere datastores ...")
371
+ spinner_name = self.get_random_spinner_name()
372
+ with Spinner(spin_prompt, spinner_name):
373
+ _get_datastores()
374
+ sys.stdout.write(" " * len(spin_prompt))
375
+ sys.stdout.write("\r")
376
+ sys.stdout.flush()
377
+
378
+ if self.verbose > 0:
379
+ LOG.debug(_("Found datastores:") + "\n" + pp(self.datastores.as_list()))
380
+
381
+ # -------------------------------------------------------------------------
382
+ def search_for_space(self):
383
+ """Search in evaluated datastore clusters and datastores for space for a virtual disk."""
384
+ if self.ds_clusters:
385
+ LOG.info(_("Searching for space in evaluated datastore clusters."))
386
+ # LOG.debug(
387
+ # f"Datastore cluster must be connected with computing cluster {self.cluster!r}")
388
+ try:
389
+ ds_cluster = self.ds_clusters.search_space(
390
+ needed_gb=self.disk_size_gb,
391
+ storage_type=self.storage_type,
392
+ reserve_space=False,
393
+ compute_cluster=self.cluster,
394
+ )
395
+
396
+ msg = "\n " + self.colored("*", "GREEN") + " "
397
+ msg += _("Found usable datastore cluster:") + "\n\n"
398
+ msg += " " + self.colored(ds_cluster, "CYAN") + "\n"
399
+
400
+ print(msg)
401
+ self.exit(0)
402
+ except VSphereNoDsClusterFoundError as e:
403
+ print()
404
+ LOG.warn(str(e))
405
+
406
+ if self.datastores:
407
+ LOG.info(_("Searching for space in evaluated datastores."))
408
+ # LOG.debug(f"Datastore must be connected with computing cluster {self.cluster!r}")
409
+ try:
410
+ datastore = self.datastores.search_space(
411
+ needed_gb=self.disk_size_gb,
412
+ storage_type=self.storage_type,
413
+ reserve_space=False,
414
+ compute_cluster=self.cluster,
415
+ use_local=True,
416
+ )
417
+
418
+ msg = "\n " + self.colored("*", "GREEN") + " "
419
+ msg += _("Found usable datastore:") + "\n\n"
420
+ msg += " " + self.colored(datastore, "CYAN") + "\n"
421
+
422
+ print(msg)
423
+ self.exit(0)
424
+ except VSphereNoDatastoreFoundError as e:
425
+ print()
426
+ LOG.warn(str(e))
427
+
428
+ print()
429
+ LOG.warn(_("No datastore cluster or datastore for the given volume."))
430
+ self.exit(3)
431
+
432
+ # -------------------------------------------------------------------------
433
+ def post_run(self):
434
+ """Execute some actions after the main routine."""
435
+ super(SearchStorageApp, self).post_run()
436
+
437
+ self.cur_vsphere = None
438
+
439
+
440
+ # =============================================================================
441
+ def main():
442
+ """Entrypoint for search-vsphere-storage."""
443
+ my_path = pathlib.Path(__file__)
444
+ appname = my_path.name
445
+
446
+ locale.setlocale(locale.LC_ALL, "")
447
+
448
+ app = SearchStorageApp(appname=appname)
449
+ app.initialized = True
450
+
451
+ if app.verbose > 2:
452
+ print(_("{c}-Object:\n{a}").format(c=app.__class__.__name__, a=app), file=sys.stderr)
453
+
454
+ try:
455
+ app()
456
+ except KeyboardInterrupt:
457
+ print("\n" + app.colored(_("User interrupt."), "YELLOW"))
458
+ sys.exit(5)
459
+
460
+ return 0
461
+
462
+
463
+ # =============================================================================
464
+ if __name__ == "__main__":
465
+
466
+ pass
467
+
468
+ # =============================================================================
469
+
470
+ # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
@@ -0,0 +1,78 @@
1
+ """
2
+ @summary: A module containing some useful argparse actions.
3
+
4
+ @author: Frank Brehm
5
+ @contact: frank@brehm-online.com
6
+ @copyright: © 2026 by Frank Brehm, Berlin
7
+ """
8
+
9
+ from __future__ import absolute_import
10
+
11
+ # Standard modules
12
+ import argparse
13
+ import logging
14
+
15
+ # import os
16
+ # from pathlib import Path
17
+
18
+ # Third party modules
19
+ # from fb_tools.common import is_sequence
20
+
21
+ # Own modules
22
+ from .xlate import XLATOR
23
+
24
+ __version__ = "0.1.2"
25
+ LOG = logging.getLogger(__name__)
26
+
27
+ _ = XLATOR.gettext
28
+
29
+
30
+ # =============================================================================
31
+ class NonNegativeIntegerOptionAction(argparse.Action):
32
+ """
33
+ It's an argparse action class to ensure a positive integer value.
34
+
35
+ It ensures, that the given value is an integer value, which ist greater or equal to 0.
36
+ """
37
+
38
+ # -------------------------------------------------------------------------
39
+ def __init__(self, option_strings, may_zero=True, *args, **kwargs):
40
+ """Initialize the NonNegativeIntegerOptionAction object."""
41
+ self.may_zero = bool(may_zero)
42
+
43
+ super(NonNegativeIntegerOptionAction, self).__init__(
44
+ *args,
45
+ **kwargs,
46
+ option_strings=option_strings,
47
+ )
48
+
49
+ # -------------------------------------------------------------------------
50
+ def __call__(self, parser, namespace, value, option_string=None):
51
+ """Check the given value from command line for type and the valid range."""
52
+ try:
53
+ val = int(value)
54
+ except Exception as e:
55
+ msg = _("Got a {c} for converting {v!r} into an integer value: {e}").format(
56
+ c=e.__class__.__name__, v=value, e=e
57
+ )
58
+ raise argparse.ArgumentError(self, msg)
59
+
60
+ if val < 0:
61
+ msg = _("The option must not be negative (given: {}).").format(value)
62
+ raise argparse.ArgumentError(self, msg)
63
+
64
+ if not self.may_zero and val == 0:
65
+ msg = _("The option must not be zero.")
66
+ raise argparse.ArgumentError(self, msg)
67
+
68
+ setattr(namespace, self.dest, val)
69
+
70
+
71
+ # =============================================================================
72
+ if __name__ == "__main__":
73
+
74
+ pass
75
+
76
+ # =============================================================================
77
+
78
+ # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
fb_vmware/base.py CHANGED
@@ -38,7 +38,7 @@ from .errors import VSphereUnsufficientCredentials
38
38
  from .errors import VSphereVimFault
39
39
  from .xlate import XLATOR
40
40
 
41
- __version__ = "1.1.2"
41
+ __version__ = "1.2.0"
42
42
 
43
43
  LOG = logging.getLogger(__name__)
44
44
 
@@ -308,6 +308,33 @@ class BaseVsphereHandler(HandlingObject):
308
308
 
309
309
  return obj
310
310
 
311
+ # -------------------------------------------------------------------------
312
+ def get_all_objects(self, content, vimtype, name):
313
+ """Get all appropriate pyvomomi objects with the given criteria."""
314
+ objects = []
315
+
316
+ container = content.viewManager.CreateContainerView(content.rootFolder, vimtype, True)
317
+ for c in container.view:
318
+ if c.name == name:
319
+ objects.append(c)
320
+
321
+ return objects
322
+
323
+ # -------------------------------------------------------------------------
324
+ def get_parents(self, managed_object):
325
+ """Get the parents of a managed object as an array."""
326
+ parents = []
327
+ if hasattr(managed_object, "parent") and managed_object.parent is not None:
328
+ parent = managed_object.parent
329
+ grand_parents = self.get_parents(parent)
330
+ if grand_parents:
331
+ parents = grand_parents
332
+ parents += [(parent.__class__.__name__, parent.name)]
333
+
334
+ return parents
335
+
336
+ return None
337
+
311
338
  # -------------------------------------------------------------------------
312
339
  def __del__(self):
313
340
  """Destroy the current Python object in this magic method."""