"""
Tool to manage the settings of Cobbler without the daemon running.
"""
# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: 2021 Dominik Gedon <dgedon@suse.de>
# SPDX-FileCopyrightText: 2021 Enno Gotthold <egotthold@suse.de>
# SPDX-FileCopyrightText: 2022 Pablo Suárez Hernández <psuarezhernandez@suse.de>
# SPDX-FileCopyrightText: Copyright SUSE LLC
import argparse
from typing import Any, Dict, List
from typing import Optional as TOptional
import yaml
from schema import Optional, SchemaError # type: ignore
from cobbler import settings
from cobbler.settings import migrations
from cobbler.settings.migrations import EMPTY_VERSION, helper
from cobbler.utils import input_converters
# TODO: Transform this into a lamda/list comprehension
def __generate_version_choices() -> List[str]:
versions = migrations.VERSION_LIST.keys()
result: List[str] = []
for version in versions:
result.append(f"{version.major}.{version.minor}.{version.patch}")
return result
def __check_settings(filepath: str, settings_to_preserve: List[str]) -> Dict[str, Any]:
try:
result = settings.read_yaml_file(filepath)
except (FileNotFoundError, yaml.YAMLError) as error:
print(str(error))
return {}
settings_version = migrations.get_settings_file_version(
result, settings_to_preserve
)
if settings_version == EMPTY_VERSION:
print("Error detecting settings file version!")
return {}
print(f"The following version was detected: {settings_version}")
return result
def __update_settings(
yaml_dict: Dict[str, Any], filepath: str, settings_to_preserve: List[str]
) -> int:
if not settings.update_settings_file(yaml_dict, filepath, settings_to_preserve):
print("Modification would make settings invalid!")
return 1
print("Updated settings file successfully!")
return 0
[docs]
def validate(args: argparse.Namespace) -> int:
"""
Validates the Cobbler settings file against the specified version schema.
:param args: argparse.Namespace containing the required attributes.
"""
try:
result = settings.read_yaml_file(args.config)
except (FileNotFoundError, yaml.YAMLError) as error:
print(str(error))
return 1
# Exclude settings to preserve from the list of settings to migrate
result, _ = migrations.filter_settings_to_validate(result, args.preserve_settings)
version_split = args.version.split(".")
version = migrations.CobblerVersion(*version_split)
try:
migrations.VERSION_LIST[version].normalize(result)
except SchemaError as error:
print("Settings file invalid!")
print(str(error))
return 1
print("Settings file successfully validated!")
return 0
[docs]
def migrate(args: argparse.Namespace) -> int:
"""
Migrate settings file to a specified version, optionally updating the target file.
:param args: argparse.Namespace containing migration options and arguments.
"""
settings_dict = __check_settings(args.config, args.preserve_settings)
if not settings_dict:
print("Settings file invalid!")
return 1
old = migrations.get_settings_file_version(settings_dict, args.preserve_settings)
if old == migrations.EMPTY_VERSION:
print("Settings file version could not be discovered!")
return 1
new_list = args.new.split(".")
new = migrations.CobblerVersion(*new_list)
result_settings = migrations.migrate(
settings_dict, args.config, old, new, args.preserve_settings
)
# only if --target supplied
if args.target is not None:
return __update_settings(result_settings, args.target, args.preserve_settings)
# if --target not supplied
print(yaml.dump(result_settings))
return 0
[docs]
def automigrate(args: argparse.Namespace) -> int:
"""
Automigrates settings based on provided arguments.
:param args: argparse.Namespace containing configuration and migration options.
"""
settings_dict = __check_settings(args.config, args.preserve_settings)
if not settings_dict:
print("Settings file invalid!")
return 1
setting_obj = helper.Setting("auto_migrate_settings", args.enable_automigration)
helper.key_set_value(setting_obj, settings_dict)
return __update_settings(settings_dict, args.config, args.preserve_settings)
[docs]
def modify(args: argparse.Namespace) -> int:
"""
Modify a single configuration setting based on the provided arguments.
:param args: argparse.Namespace containing configuration and migration options.
"""
# pylint: disable=protected-access
# Disable protected-access because we can't use the property that is available in newer versions of the schema
# library.
settings_dict = __check_settings(args.config, args.preserve_settings)
if not settings_dict:
print("Settings file invalid!")
return 1
schema = migrations.get_schema(migrations.get_installed_version())
setting_obj = helper.Setting(args.key, args.value)
# _schema used due to old version of python3-schema in Leap
try:
key_type = schema._schema[setting_obj.key_name] # type: ignore
except KeyError:
try:
key_type = schema._schema[Optional(setting_obj.key_name)] # type: ignore
except KeyError:
print("Requested key not found in the settings!")
return 1
if key_type not in (bool, int, float, str, dict, list):
if isinstance(key_type, list):
key_type = list
elif isinstance(key_type, dict):
key_type = dict
else:
print("Unsupported type!")
if isinstance(Optional, key_type):
key_type = schema._schema[setting_obj.key_name].name # type: ignore
if key_type == bool:
setting_obj.value = input_converters.input_boolean(setting_obj.value)
elif key_type == int:
setting_obj.value = int(setting_obj.value)
elif key_type == float:
setting_obj.value = float(setting_obj.value)
elif key_type == list:
setting_obj.value = input_converters.input_string_or_list(setting_obj.value)
elif key_type == dict:
setting_obj.value = input_converters.input_string_or_dict(setting_obj.value)
elif key_type == str:
setting_obj.value = setting_obj.value
else:
print("Unsupported type!")
return 1
helper.key_set_value(setting_obj, settings_dict)
return __update_settings(settings_dict, args.config, args.preserve_settings)
parser = argparse.ArgumentParser(
description="Manage the settings of Cobbler without a running daemon."
)
parser.add_argument(
"-c",
"--config",
help="The location of the Cobbler configuration file.",
default="/etc/cobbler/settings.yaml",
)
parser.add_argument(
"--preserve-settings",
help="Comma separeted list of custom settings to preserve during migration",
dest="preserve_settings",
type=lambda arg: arg.split(","),
default=[],
)
subparsers = parser.add_subparsers(
title="Subcommands", help="One of these commands is required."
)
parser_validate = subparsers.add_parser(
"validate",
description="Validates if Cobbler would start with the current settings file.",
)
parser_validate.set_defaults(func=validate)
parser_validate.add_argument(
"-v",
"--version",
help="The version to validate against.",
choices=sorted(__generate_version_choices()),
default=sorted(__generate_version_choices())[-1],
)
parser_migrate = subparsers.add_parser(
"migrate",
description="Migrates from the current version to the version provided with "
'"--new".',
)
parser_migrate.set_defaults(func=migrate)
# TODO: Implement in the future
# parser_migrate.add_argument("--diff", "-d",
# help="Whether to produce a diff output or not.",
# action="store_true",
# default=False)
parser_migrate.add_argument(
"-t",
"--target",
help="Write the resulting settings to the target path given in this argument.",
)
parser_migrate.add_argument(
"--new",
help="The new version to migrate to, e.g. 3.3.0",
dest="new",
choices=sorted(__generate_version_choices()),
default=sorted(__generate_version_choices())[-1],
)
parser_automigrate = subparsers.add_parser(
"automigrate",
description="Enables or disables the automigration on startup of the daemon. "
'If no flag is provided, the default is "--disable".',
)
parser_automigrate.set_defaults(func=automigrate)
parser_automigrate.add_argument(
"-e",
"--enable",
help="Enables settings automigration.",
dest="enable_automigration",
action="store_true",
)
parser_automigrate.add_argument(
"-d",
"--disable",
help="Disables settings automigration.",
dest="enable_automigration",
action="store_false",
)
parser_modify = subparsers.add_parser(
"modify", description="Modify the value of a key in the config file."
)
parser_modify.set_defaults(func=modify)
parser_modify.add_argument(
"-k",
"--key",
help="The name of the key in file to edit. If the key is nested use the format "
'"parent_key.key".',
required=True,
)
parser_modify.add_argument(
"-v", "--value", help="The new value of the key.", required=True
)
[docs]
def main(args: TOptional[List[str]] = None) -> int:
"""
Main entrypoint for the the ``cobbler-settings`` script.
"""
parsed_args = parser.parse_args(args=args)
if hasattr(parsed_args, "func"):
return parsed_args.func(parsed_args)
parser.print_help()
return 0