Source code for cobbler.items.image

"""
Cobbler module that contains the code for a Cobbler image object.

Changelog:

V3.4.0 (unreleased):
    * Added:
        * ``display_name``
    * Changed:
        * Constructor: ``kwargs`` can now be used to seed the item during creation.
        * ``autoinstall``: Restored inheritance of the property.
        * ``children``: The proqperty was moved to the base class.
        * ``from_dict()``: The method was moved to the base class.
        * ``virt_disk_driver``: Restored inheritance of the property.
        * ``virt_ram``: Restored inheritance of the property.
        * ``virt_type``: Restored inheritance of the property.
        * ``virt_bridge``: Restored inheritance of the property.
V3.3.4 (unreleased):
    * No changes
V3.3.3:
    * Added:
        * ``children``
    * Changes:
        * ``virt_file_size``: Inherits from the settings again
        * ``boot_loaders``: Inherits from the settings again
V3.3.2:
    * No changes
V3.3.1:
    * No changes
V3.3.0:
    * This release switched from pure attributes to properties (getters/setters).
    * Added:
        * ``boot_loaders``: list
        * ``menu``: str
        * ``supported_boot_loaders``: list
        * ``from_dict()``
    * Moved to parent class (Item):
        * ``ctime``: float
        * ``mtime``: float
        * ``depth``: int
        * ``parent``: str
        * ``uid``: str
        * ``comment``: str
        * ``name``: str
    * Removed:
        * ``get_fields()``
        * ``get_parent()``
        * ``set_arch()`` - Please use the ``arch`` property.
        * ``set_autoinstall()`` - Please use the ``autoinstall`` property.
        * ``set_file()`` - Please use the ``file`` property.
        * ``set_os_version()`` - Please use the ``os_version`` property.
        * ``set_breed()`` - Please use the ``breed`` property.
        * ``set_image_type()`` - Please use the ``image_type`` property.
        * ``set_virt_cpus()`` - Please use the ``virt_cpus`` property.
        * ``set_network_count()`` - Please use the ``network_count`` property.
        * ``set_virt_auto_boot()`` - Please use the ``virt_auto_boot`` property.
        * ``set_virt_file_size()`` - Please use the ``virt_file_size`` property.
        * ``set_virt_disk_driver()`` - Please use the ``virt_disk_driver`` property.
        * ``set_virt_ram()`` - Please use the ``virt_ram`` property.
        * ``set_virt_type()`` - Please use the ``virt_type`` property.
        * ``set_virt_bridge()`` - Please use the ``virt_bridge`` property.
        * ``set_virt_path()`` - Please use the ``virt_path`` property.
        * ``get_valid_image_types()``
    * Changes:
        * ``arch``: str -> enums.Archs
        * ``autoinstall``: str -> enums.VALUE_INHERITED
        * ``image_type``: str -> enums.ImageTypes
        * ``virt_auto_boot``: Union[bool, SETTINGS:virt_auto_boot] -> bool
        * ``virt_bridge``: Union[str, SETTINGS:default_virt_bridge] -> str
        * ``virt_disk_driver``: Union[str, SETTINGS:default_virt_disk_driver] -> enums.VirtDiskDrivers
        * ``virt_file_size``: Union[float, SETTINGS:default_virt_file_size] -> float
        * ``virt_ram``: Union[int, SETTINGS:default_virt_ram] -> int
        * ``virt_type``: Union[str, SETTINGS:default_virt_type] -> enums.VirtType
V3.2.2:
    * No changes
V3.2.1:
    * Added:
        * ``kickstart``: Resolves as a proxy to ``autoinstall``
V3.2.0:
    * No changes
V3.1.2:
    * No changes
V3.1.1:
    * No changes
V3.1.0:
    * No changes
V3.0.1:
    * No changes
V3.0.0:
    * Added:
        * ``set_autoinstall()``
    * Changes:
        * Rename: ``kickstart`` -> ``autoinstall``
    * Removed:
        * ``set_kickstart()`` - Please use ``set_autoinstall()``
V2.8.5:
    * Inital tracking of changes for the changelog.
    * Added:
        * ``ctime``: float
        * ``depth``: int
        * ``mtime``: float
        * ``parent``: str
        * ``uid``: str

        * ``arch``: str
        * ``kickstart``: str
        * ``breed``: str
        * ``comment``: str
        * ``file``: str
        * ``image_type``: str
        * ``name``: str
        * ``network_count``: int
        * ``os_version``: str
        * ``owners``: Union[list, SETTINGS:default_ownership]
        * ``virt_auto_boot``: Union[bool, SETTINGS:virt_auto_boot]
        * ``virt_bridge``: Union[str, SETTINGS:default_virt_bridge]
        * ``virt_cpus``: int
        * ``virt_disk_driver``: Union[str, SETTINGS:default_virt_disk_driver]
        * ``virt_file_size``: Union[float, SETTINGS:default_virt_file_size]
        * ``virt_path``: str
        * ``virt_ram``: Union[int, SETTINGS:default_virt_ram]
        * ``virt_type``: Union[str, SETTINGS:default_virt_type]
"""

# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: Copyright 2006-2009, Red Hat, Inc and Others
# SPDX-FileCopyrightText: Michael DeHaan <michael.dehaan AT gmail>

import copy
from typing import TYPE_CHECKING, Any, List, Optional, Union

from cobbler import enums, validate
from cobbler.cexceptions import CX
from cobbler.items.abstract.bootable_item import BootableItem
from cobbler.items.options.virt import VirtOption
from cobbler.utils import input_converters, signatures

if TYPE_CHECKING:
    from cobbler.api import CobblerAPI
    from cobbler.items.template import Template

    InheritableProperty = property
    LazyProperty = property
else:
    from cobbler.decorator import InheritableProperty, LazyProperty


[docs] class Image(BootableItem): """ A Cobbler Image. Tracks a virtual or physical image, as opposed to a answer file (autoinst) led installation. """ TYPE_NAME = "image" COLLECTION_TYPE = "image" def __init__(self, api: "CobblerAPI", *args: Any, **kwargs: Any) -> None: """ Constructor :param api: The Cobbler API object which is used for resolving information. """ super().__init__(api) # Prevent attempts to clear the to_dict cache before the object is initialized. self._has_initialized = False self._arch = enums.Archs.X86_64 self._autoinstall = enums.VALUE_INHERITED self._breed = "" self._file = "" self._image_type = enums.ImageTypes.DIRECT self._network_count = 0 self._os_version = "" self._supported_boot_loaders: List[enums.BootLoader] = [] self._boot_loaders: List[enums.BootLoader] = [enums.BootLoader.INHERITED] self._menu = "" self._display_name = "" self._virt = VirtOption(api=api, item=self, cpus=1, path="") self._virt_bridge = enums.VALUE_INHERITED # TODO: Evaluate missing pxe_boot and virt_bridge if len(kwargs): self.from_dict(kwargs) if not self._has_initialized: self._has_initialized = True def __getattr__(self, name: str): if name == "kickstart": return self.autoinstall raise AttributeError(f'Attribute "{name}" did not exist on object type Image.') # # override some base class methods first (BootableItem) #
[docs] def make_clone(self): """ Clone this image object. Please manually adjust all value yourself to make the cloned object unique. :return: The cloned instance of this object. """ _dict = copy.deepcopy(self.to_dict()) _dict.pop("uid", None) if isinstance(_dict.get("autoinstall"), dict): # This is a concrete dict object, not inherited. autoinstall = _dict.pop("autoinstall") _dict["autoinstall"] = autoinstall["uid"] return Image(self.api, **_dict)
# # specific methods for item.Image # @property def virt(self) -> VirtOption: """ Property for the VirtOptions. :getter: The VirtOptions of the Image. """ return self._virt @LazyProperty def arch(self) -> enums.Archs: """ Represents the architecture the image has. If deployed to a physical host this should be enforced, a virtual image may be deployed on a host with any architecture. :getter: The current architecture. Default is ``X86_64``. :setter: Should be of the enum type or str. May raise an exception in case the architecture is not known to Cobbler. """ return self._arch @arch.setter def arch(self, arch: Union[str, enums.Archs]): """ The field is mainly relevant to PXE provisioning. See comments for arch property in distro.py, this works the same. :param arch: The new architecture to set. """ old_arch = self._arch self._arch = enums.Archs.to_enum(arch) self.api.images().update_index_value(self, "arch", old_arch, self._arch) @InheritableProperty def autoinstall(self) -> Optional["Template"]: """ Represents the automatic OS installation template object. :getter: The template object that is configured. :setter: The name, UID or Template object. This property may be set to ``<<inherit>>``. """ if self._autoinstall == "": return None autoinstall = self._resolve(["autoinstall"]) if validate.validate_uuid(autoinstall): search_result = self.api.find_template(False, False, uid=autoinstall) elif hasattr(autoinstall, "TYPE_NAME"): return autoinstall else: # Built-In Templates save the names to survive serialization. search_result = self.api.find_template(False, False, name=autoinstall) if search_result is None: raise ValueError("No search result for given template UID/Name!") if isinstance(search_result, list): raise ValueError("Ambigous template match name detected!") return search_result @autoinstall.setter def autoinstall(self, autoinstall: Union[str, "Template"]): """ Setter for the ``autoinstall`` property. :param autoinstall: local automatic installation template name, UID or Template object. """ self._autoinstall = validate.validate_template(self.api, autoinstall) @LazyProperty def file(self) -> str: """ Stores the image location. This should be accessible on all nodes that need to access it. Format: can be one of the following: * username:password@hostname:/path/to/the/filename.ext * username@hostname:/path/to/the/filename.ext * hostname:/path/to/the/filename.ext * /path/to/the/filename.ext :getter: The path to the image location or an emtpy string. :setter: May raise a TypeError or SyntaxError in case the validation of the location fails. """ return self._file @file.setter def file(self, filename: str): """ The setter for the image location. :param filename: The location where the image is stored. :raises SyntaxError: In case a protocol was found. """ if not isinstance(filename, str): # type: ignore raise TypeError("file must be of type str to be parsable.") if not filename: self._file = "" return # validate file location format if filename.find("://") != -1: raise SyntaxError( "Invalid image file path location, it should not contain a protocol" ) uri = filename auth = "" hostname = "" if filename.find("@") != -1: auth, filename = filename.split("@") # extract the hostname # 1. if we have a colon, then everything before it is a hostname # 2. if we don't have a colon, there is no hostname if filename.find(":") != -1: hostname, filename = filename.split(":") elif filename[0] != "/": raise SyntaxError(f"invalid file: {filename}") # raise an exception if we don't have a valid path if len(filename) > 0 and filename[0] != "/": raise SyntaxError(f"file contains an invalid path: {filename}") if filename.find("/") != -1: _, filename = filename.rsplit("/", 1) if len(filename) == 0: raise SyntaxError("missing filename") if len(auth) > 0 and len(hostname) == 0: raise SyntaxError( "a hostname must be specified with authentication details" ) self._file = uri @LazyProperty def os_version(self) -> str: r""" The operating system version which the image contains. :getter: The sanitized operating system version. :setter: Accepts a str which will be validated against the ``distro_signatures.json``. """ return self._os_version @os_version.setter def os_version(self, os_version: str): """ Set the operating system version with this setter. :param os_version: This must be a valid OS-Version. """ self._os_version = validate.validate_os_version(os_version, self.breed) @LazyProperty def breed(self) -> str: r""" The operating system breed. :getter: Returns the current breed. :setter: When setting this it is validated against the ``distro_signatures.json`` file. """ return self._breed @breed.setter def breed(self, breed: str): """ Set the operating system breed with this setter. :param breed: The breed of the operating system which is available in the image. """ self._breed = validate.validate_breed(breed) @LazyProperty def image_type(self) -> enums.ImageTypes: """ Indicates what type of image this is. direct = something like "memdisk", physical only iso = a bootable ISO that pxe's or can be used for virt installs, virtual only virt-clone = a cloned virtual disk (FIXME: not yet supported), virtual only memdisk = hdd image (physical only) :getter: The enum type value of the image type. :setter: Accepts str like and enum type values and raises a TypeError or ValueError in the case of a problem. """ return self._image_type @image_type.setter def image_type(self, image_type: Union[enums.ImageTypes, str]): """ The setter which accepts enum type or str type values. Latter ones will be automatically converted if possible. :param image_type: One of the four options from above. :raises TypeError: In case a disallowed type was found. :raises ValueError: In case the conversion from str could not successfully executed. """ if not isinstance(image_type, (enums.ImageTypes, str)): # type: ignore raise TypeError("image_type must be of type str or enum.ImageTypes") if isinstance(image_type, str): if not image_type: # FIXME: Add None Image type self._image_type = enums.ImageTypes.DIRECT try: image_type = enums.ImageTypes[image_type.upper()] except KeyError as error: raise ValueError( f"image_type choices include: {list(map(str, enums.ImageTypes))}" ) from error # str was converted now it must be an enum.ImageTypes if not isinstance(image_type, enums.ImageTypes): # type: ignore raise TypeError("image_type needs to be of type enums.ImageTypes") if image_type not in enums.ImageTypes: raise ValueError( f"image type must be one of the following: {', '.join(list(map(str, enums.ImageTypes)))}" ) self._image_type = image_type @LazyProperty def network_count(self) -> int: """ Represents the number of virtual NICs this image has. .. deprecated:: 3.3.0 This is nowhere used in the project and will be removed in a future release. :getter: The number of networks. :setter: Raises a ``TypeError`` in case the value is not an int. """ return self._network_count @network_count.setter def network_count(self, network_count: Union[int, str]): """ Setter for the number of networks. :param network_count: If None or emtpy will be set to ``1``, otherwise the given integer value will be set. :raises TypeError: In case the network_count was not of type int. """ if network_count is None or network_count == "": # type: ignore network_count = 1 if not isinstance(network_count, int): # type: ignore raise TypeError( "Field network_count of object image needs to be of type int." ) self._network_count = network_count @InheritableProperty def virt_bridge(self) -> str: r""" The name of the virtual bridge used for networking. .. warning:: The new validation for the setter is not working. Thus the inheritance from the settings is broken. :getter: The name of the bridge. :setter: The new name of the bridge. If set to an empty ``str``, it will be taken from the settings. """ return self._resolve(["virt_bridge"]) @virt_bridge.setter def virt_bridge(self, vbridge: str): """ Setter for the virtual bridge which is used. :param vbridge: The name of the virtual bridge to use. """ self._virt_bridge = validate.validate_virt_bridge(vbridge) @LazyProperty def menu(self) -> str: """ Property to represent the menu which this image should be put into. :getter: The name of the menu or an emtpy str. :setter: Should only be the name of the menu not the object. May raise ``CX`` in case the menu does not exist. """ return self._menu @menu.setter def menu(self, menu: str): """ Setter for the menu property. :param menu: The menu for the image. :raises CX: In case the menu to be set could not be found. """ if menu and menu != "": menu_list = self.api.menus() if not menu_list.find(uid=menu): raise CX(f"menu {menu} not found") old_menu = self._menu self._menu = menu self.api.images().update_index_value(self, "menu", old_menu, menu) @LazyProperty def display_name(self) -> str: """ Returns the display name. :getter: Returns the display name for the boot menu. :setter: Sets the display name for the boot menu. """ return self._display_name @display_name.setter def display_name(self, display_name: str): """ Setter for the display_name of the item. :param display_name: The new display_name. If ``None`` the display_name will be set to an emtpy string. """ self._display_name = display_name @property def supported_boot_loaders(self) -> List[enums.BootLoader]: """ Read only property which represents the subset of settable bootloaders. :getter: The bootloaders which are available for being set. """ if len(self._supported_boot_loaders) == 0: self._supported_boot_loaders = signatures.get_supported_distro_boot_loaders( self ) return self._supported_boot_loaders @InheritableProperty def boot_loaders(self) -> List[enums.BootLoader]: """ Represents the boot loaders which are able to boot this image. :getter: The bootloaders. May be an emtpy list. :setter: A list with the supported boot loaders for this image. """ if self._boot_loaders == [enums.BootLoader.INHERITED]: return self.supported_boot_loaders # The following line is missleading for pyright since it doesn't understand # that we use only a constant with str type. return self._boot_loaders # type: ignore @boot_loaders.setter def boot_loaders( self, boot_loaders: Union[str, List[str], List[enums.BootLoader]], ): """ Setter of the boot loaders. :param boot_loaders: The boot loaders for the image. :raises TypeError: In case this was of a not allowed type. :raises ValueError: In case the str which contained the list could not be successfully split. """ boot_loaders_split: List[enums.BootLoader] = [] if isinstance(boot_loaders, list): if all([isinstance(value, str) for value in boot_loaders]): boot_loaders_split = [ enums.BootLoader.to_enum(value) for value in boot_loaders ] elif all([isinstance(value, enums.BootLoader) for value in boot_loaders]): boot_loaders_split = boot_loaders # type: ignore else: raise TypeError( "The items inside the list of boot_loaders must all be of type str or cobbler.enums.BootLoader" ) elif isinstance(boot_loaders, str): # type: ignore if boot_loaders == "": self._boot_loaders = [] return if boot_loaders == enums.VALUE_INHERITED: self._boot_loaders = [enums.BootLoader.INHERITED] return boot_loaders_split = [ enums.BootLoader.to_enum(value) for value in input_converters.input_string_or_list_no_inherit( boot_loaders ) ] else: raise TypeError("The bootloaders need to be either a str or list") if ( len(boot_loaders_split) == 1 and boot_loaders_split[0] == enums.BootLoader.INHERITED ): self._boot_loaders = [enums.BootLoader.INHERITED] return if not set(boot_loaders).issubset(self.supported_boot_loaders): raise ValueError( f"Invalid boot loader names: {boot_loaders}. Supported boot loaders are:" f" {' '.join([value.value for value in self.supported_boot_loaders])}" ) self._boot_loaders = boot_loaders_split