"""
Migration from V3.3.3 to V3.4.0
"""
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: 2022 Dominik Gedon <dgedon@suse.de>
# SPDX-FileCopyrightText: Copyright SUSE LLC
import configparser
import glob
import json
import os
import pathlib
import uuid
from configparser import ConfigParser
from typing import Any, Dict
from schema import Optional, Schema, SchemaError # type: ignore
from cobbler.settings.migrations import V3_3_7, helper
schema = Schema(
{
Optional("auto_migrate_settings"): bool,
Optional("allow_duplicate_hostnames"): bool,
Optional("allow_duplicate_ips"): bool,
Optional("allow_duplicate_macs"): bool,
Optional("allow_dynamic_settings"): bool,
Optional("always_write_dhcp_entries"): bool,
Optional("anamon_enabled"): bool,
Optional("auth_token_expiration"): int,
Optional("authn_pam_service"): str,
Optional("autoinstall_templates_dir"): str,
Optional("bind_chroot_path"): str,
Optional("bind_zonefile_path"): str,
Optional("bind_master"): str,
Optional("bootloaders_dir"): str,
Optional("bootloaders_formats"): dict,
Optional("bootloaders_modules"): list,
Optional("bootloaders_shim_folder"): str,
Optional("bootloaders_shim_file"): str,
Optional("secure_boot_grub_folder"): str,
Optional("secure_boot_grub_file"): str,
Optional("bootloaders_ipxe_folder"): str,
Optional("syslinux_dir"): str,
Optional("syslinux_memdisk_folder"): str,
Optional("syslinux_pxelinux_folder"): str,
Optional("genders_settings_file"): str,
Optional("grub2_mod_dir"): str,
Optional("grubconfig_dir"): str,
Optional("build_reporting_enabled"): bool,
Optional("build_reporting_email"): [str],
Optional("build_reporting_ignorelist"): [str],
Optional("build_reporting_sender"): str,
Optional("build_reporting_smtp_server"): str,
Optional("build_reporting_subject"): str,
Optional("buildisodir"): str,
Optional("cheetah_import_whitelist"): [str],
Optional("client_use_https"): bool,
Optional("client_use_localhost"): bool,
Optional("cobbler_master"): str,
Optional("convert_server_to_ip"): bool,
Optional("createrepo_flags"): str,
Optional("autoinstall"): str,
Optional("default_name_servers"): [str],
Optional("default_name_servers_search"): [str],
Optional("default_ownership"): [str],
Optional("default_password_crypted"): str,
Optional("default_template_type"): str,
Optional("default_virt_bridge"): str,
Optional("default_virt_disk_driver"): str,
Optional("default_virt_file_size"): float,
Optional("default_virt_ram"): int,
Optional("default_virt_type"): str,
Optional("dnsmasq_ethers_file"): str,
Optional("dnsmasq_hosts_file"): str,
Optional("dnsmasq_settings_file"): str,
Optional("enable_ipxe"): bool,
Optional("enable_menu"): bool,
Optional("extra_settings_list"): [str],
Optional("http_port"): int,
Optional("kernel_options"): dict,
Optional("ldap_anonymous_bind"): bool,
Optional("ldap_base_dn"): str,
Optional("ldap_port"): int,
Optional("ldap_search_bind_dn"): str,
Optional("ldap_search_passwd"): str,
Optional("ldap_search_prefix"): str,
Optional("ldap_server"): str,
Optional("ldap_tls"): bool,
Optional("ldap_tls_cacertdir"): str,
Optional("ldap_tls_cacertfile"): str,
Optional("ldap_tls_certfile"): str,
Optional("ldap_tls_keyfile"): str,
Optional("ldap_tls_reqcert"): str,
Optional("ldap_tls_cipher_suite"): str,
Optional("bind_manage_ipmi"): bool,
Optional("manage_dhcp_v4"): bool,
Optional("manage_dhcp_v6"): bool,
Optional("manage_dns"): bool,
Optional("manage_forward_zones"): [str],
Optional("manage_reverse_zones"): [str],
Optional("manage_genders"): bool,
Optional("manage_rsync"): bool,
Optional("manage_tftpd"): bool,
Optional("next_server_v4"): str,
Optional("next_server_v6"): str,
Optional("ndjbdns_data_file"): str,
Optional("nsupdate_enabled"): bool,
Optional("nsupdate_log"): str,
Optional("nsupdate_tsig_algorithm"): str,
Optional("nsupdate_tsig_key"): [str],
Optional("power_management_default_type"): str,
Optional("proxies"): [str],
Optional("proxy_url_ext"): str,
Optional("proxy_url_int"): str,
Optional("puppet_auto_setup"): bool,
Optional("puppet_parameterized_classes"): bool,
Optional("puppet_server"): str,
Optional("puppet_version"): int,
Optional("puppetca_path"): str,
Optional("pxe_just_once"): bool,
Optional("nopxe_with_triggers"): bool,
Optional("redhat_management_permissive"): bool,
Optional("redhat_management_server"): str,
Optional("redhat_management_key"): str,
Optional("redhat_management_org"): str,
Optional("redhat_management_user"): str,
Optional("redhat_management_password"): str,
Optional("uyuni_authentication_endpoint"): str,
Optional("register_new_installs"): bool,
Optional("remove_old_puppet_certs_automatically"): bool,
Optional("replicate_repo_rsync_options"): str,
Optional("replicate_rsync_options"): str,
Optional("reposync_flags"): str,
Optional("reposync_rsync_flags"): str,
Optional("restart_dhcp"): bool,
Optional("restart_dns"): bool,
Optional("run_install_triggers"): bool,
Optional("scm_track_enabled"): bool,
Optional("scm_track_mode"): str,
Optional("scm_track_author"): str,
Optional("scm_push_script"): str,
Optional("serializer_pretty_json"): bool,
Optional("server"): str,
Optional("sign_puppet_certs_automatically"): bool,
Optional("signature_path"): str,
Optional("signature_url"): str,
Optional("tftpboot_location"): str,
Optional("virt_auto_boot"): bool,
Optional("webdir"): str,
Optional("webdir_whitelist"): [str],
Optional("xmlrpc_port"): int,
Optional("yum_distro_priority"): int,
Optional("yum_post_install_mirror"): bool,
Optional("yumdownloader_flags"): str,
Optional("windows_enabled"): bool,
Optional("windows_wimupdate_location"): str,
Optional("samba_distro_share"): str,
Optional("modules"): {
Optional("authentication"): {
Optional("module"): str,
Optional("hash_algorithm"): str,
},
Optional("authorization"): {Optional("module"): str},
Optional("dns"): {Optional("module"): str},
Optional("dhcp"): {Optional("module"): str},
Optional("tftpd"): {Optional("module"): str},
Optional("serializers"): {Optional("module"): str},
},
Optional("mongodb"): {
Optional("host"): str,
Optional("port"): int,
},
Optional("cache_enabled"): bool,
Optional("autoinstall_scheme"): str,
Optional("lazy_start"): bool,
Optional("memory_indexes"): {
Optional("distro"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("arch"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("image"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("arch"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("menu"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("menu"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("parent"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("network_interface"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("mac_address"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("ipv4.address"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("ipv6.address"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("dns.name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("profile"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("parent"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("distro"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("arch"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("menu"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("repos"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("repo"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("system"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("image"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
Optional("profile"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("distro_group"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("profile_group"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
Optional("system_group"): {
Optional("name"): {
Optional("property"): str,
Optional("nonunique"): bool,
Optional("disabled"): bool,
},
},
},
}, # type: ignore
ignore_extra_keys=False,
)
[docs]
def validate(settings: Dict[str, Any]) -> bool:
"""
Checks that a given settings dict is valid according to the reference V3.4.0 schema ``schema``.
:param settings: The settings dict to validate.
:return: True if valid settings dict otherwise False.
"""
try:
schema.validate(settings) # type: ignore
except SchemaError:
return False
return True
[docs]
def normalize(settings: Dict[str, Any]) -> Dict[str, Any]:
"""
If data in ``settings`` is valid the validated data is returned.
:param settings: The settings dict to validate.
:return: The validated dict.
"""
# We are aware of our schema and thus can safely ignore this.
return schema.validate(settings) # type: ignore
[docs]
def migrate(settings: Dict[str, Any]) -> Dict[str, Any]:
"""
Migration of the settings ``settings`` to version V3.4.0 settings
:param settings: The settings dict to migrate
:return: The migrated dict
"""
if not V3_3_7.validate(settings):
raise SchemaError("V3.3.7: Schema error while validating")
# rename keys and update their value if needed
include = settings.pop("include")
settings.pop("mgmt_classes")
settings.pop("mgmt_parameters")
settings.pop("manage_dhcp")
jinja2_includedir = settings.pop("jinja2_includedir")
iso_template_dir = settings.pop("iso_template_dir")
boot_loader_conf_template_dir = settings.pop("boot_loader_conf_template_dir")
autoinstall_snippets_dir = settings.pop("autoinstall_snippets_dir")
# Do mongodb.conf migration
mongodb_config = "/etc/cobbler/mongodb.conf"
modules_config_parser = ConfigParser()
try:
modules_config_parser.read(mongodb_config)
except configparser.Error as cp_error:
raise configparser.Error(
"Could not read Cobbler MongoDB config file!"
) from cp_error
settings["mongodb"] = {
"host": modules_config_parser.get("connection", "host", fallback="localhost"),
"port": modules_config_parser.getint("connection", "port", fallback=27017),
}
mongodb_config_path = pathlib.Path(mongodb_config)
if mongodb_config_path.exists():
mongodb_config_path.unlink()
# Do mongodb.conf migration
modules_config = "/etc/cobbler/modules.conf"
modules_config_parser = ConfigParser()
try:
modules_config_parser.read(mongodb_config)
except configparser.Error as cp_error:
raise configparser.Error(
"Could not read Cobbler modules.conf config file!"
) from cp_error
settings["modules"] = {
"authentication": {
"module": modules_config_parser.get(
"authentication", "module", fallback="authentication.configfile"
),
"hash_algorithm": modules_config_parser.get(
"authentication", "hash_algorithm", fallback="sha3_512"
),
},
"authorization": {
"module": modules_config_parser.get(
"authorization", "module", fallback="authorization.allowall"
)
},
"dns": {
"module": modules_config_parser.get(
"dns", "module", fallback="managers.bind"
)
},
"dhcp": {
"module": modules_config_parser.get(
"dhcp", "module", fallback="managers.isc"
)
},
"tftpd": {
"module": modules_config_parser.get(
"tftpd", "module", fallback="managers.in_tftpd"
)
},
"serializers": {
"module": modules_config_parser.get(
"serializers", "module", fallback="serializers.file"
)
},
}
modules_config_path = pathlib.Path(modules_config)
if modules_config_path.exists():
modules_config_path.unlink()
# Migrate Jinja include directory to new location
# TODO: Implement
_ = jinja2_includedir
# Migrate ISO template directory to new location
# TODO: Implement
_ = iso_template_dir
# Migrate boot-loader conf template directory to new location
# TODO: Implement
_ = boot_loader_conf_template_dir
# Migrate autoinstall snippets directory to new location
# TODO: Implement
_ = autoinstall_snippets_dir
# Drop defaults
# pylint: disable-next=import-outside-toplevel
from cobbler.settings import Settings
helper.key_drop_if_default(settings, Settings().to_dict())
# Write settings to disk
# pylint: disable-next=import-outside-toplevel
from cobbler.settings import update_settings_file
update_settings_file(settings)
for include_path in include:
include_directory = pathlib.Path(include_path)
if include_directory.is_dir() and include_directory.exists():
include_directory.rmdir()
collection_folder = pathlib.Path("/var/lib/cobbler/collections/")
# migrate stored cobbler collections
migrate_cobbler_collections(str(collection_folder))
# Migrate JSON filenames
migrate_cobbler_json_files(collection_folder)
# Migrate SQLite DB
# TODO
# Migrate MongoDB
# TODO
# Migrate Network Interfaces to dedicated collection
migrate_cobbler_network_interfaces(collection_folder)
return normalize(settings)
[docs]
def migrate_cobbler_collections(collections_dir: str) -> None:
"""
Manipulate the main Cobbler stored collections and migrate deprecated settings
to work with newer Cobbler versions.
:param collections_dir: The directory of Cobbler where the collections files are.
"""
# Migrate changed properties
helper.backup_dir(collections_dir)
for collection_file in glob.glob(
os.path.join(collections_dir, "**/*.json"), recursive=True
):
data = None
with open(collection_file, encoding="utf-8") as _f:
data = json.loads(_f.read())
# migrate interface.interface_type from empty string to "NA"
if "interfaces" in data:
for iface in data["interfaces"]:
if data["interfaces"][iface]["interface_type"] == "":
data["interfaces"][iface]["interface_type"] = "NA"
# Remove fetchable_files from the items
if "fetchable_files" in data:
data.pop("fetchable_files", None)
# Migrate boot_files to template_files
if "boot_files" in data and "template_files" in data:
# Dicts can both be implicitly and explicitly inherited
old_boot_files = data.pop("boot_files")
if old_boot_files != "<<inherit>>":
data["template_files"] = {**data["template_files"], **old_boot_files}
with open(collection_file, "w", encoding="utf-8") as _f:
_f.write(json.dumps(data))
[docs]
def migrate_cobbler_json_files(collection_folder: pathlib.Path) -> None:
"""
Rename all JSON files from name-based files to uid-based files.
:param collection_folder: The directory of Cobbler where the collections files are.
"""
helper.backup_dir(str(collection_folder))
for folder in pathlib.Path(collection_folder).iterdir():
for file in folder.iterdir():
if not file.name.endswith(".json"):
continue
uid = json.loads(file.read_text(encoding="UTF-8")).get("uid")
file.rename(file.parent / f"{uid}.json")
[docs]
def migrate_cobbler_network_interfaces(collection_folder: pathlib.Path) -> None:
"""
Move all network interfaces from embedded system files to the dedicated collection.
:param collection_folder: The directory of Cobbler where the collections files are.
"""
for file in (collection_folder / "systems").iterdir():
if not file.name.endswith(".json"):
continue
system_dict = json.loads(file.read_text(encoding="UTF-8"))
interfaces = system_dict.pop("interfaces")
for interface_name, interface_dict in interfaces.items():
interface_uid = uuid.uuid4().hex
interface_file = (
collection_folder / "network_interfaces" / f"{interface_uid}.json"
)
# Set uid & name and system uid of the interface
interface_dict["uid"] = interface_uid
interface_dict["name"] = interface_name
interface_dict["system_uid"] = system_dict["uid"]
interface_file.write_text(json.dumps(interface_dict), encoding="UTF-8")
file.write_text(json.dumps(system_dict), encoding="UTF-8")