diff --git a/coriolis/osmorphing/conf.py b/coriolis/osmorphing/conf.py new file mode 100644 index 000000000..3c47be281 --- /dev/null +++ b/coriolis/osmorphing/conf.py @@ -0,0 +1,152 @@ +# Copyright 2026 Cloudbase Solutions Srl +# All Rights Reserved. + +"""OS morphing destination options registered with oslo.config. + +Add one entry to ``_OPTIONS`` for each new option. +``coriolis.providers.conf`` copies ``SCHEMA_PROPERTIES``, +``DESTINATION_OPTIONS``, and ``OVERRIDES``. +The option group name matches +``coriolis.providers.conf.COMMON_TARGET_OPTION_GROUP``. +""" + +from oslo_config import cfg +from oslo_log import log as logging + +from coriolis import exception + +# providers.conf uses this same group name. +COMMON_TARGET_OPTION_GROUP = "common_target_options" + +CLOUDBASE_INIT_DEFAULT_PLUGINS = [ + "cloudbaseinit.plugins.common.mtu.MTUPlugin", + "cloudbaseinit.plugins.windows.ntpclient.NTPClientPlugin", + "cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin", + "cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin", + "cloudbaseinit.plugins.windows.licensing.WindowsLicensingPlugin", + "cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin", + "cloudbaseinit.plugins.common.userdata.UserDataPlugin", + "cloudbaseinit.plugins.common.setuserpassword.SetUserPasswordPlugin", + "cloudbaseinit.plugins.windows.winrmlistener.ConfigWinRMListenerPlugin", + "cloudbaseinit.plugins.windows.winrmcertificateauth." + "ConfigWinRMCertificateAuthPlugin", + "cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin", +] + +CLOUDBASE_INIT_PLUGINS_SCHEMA = { + "type": "array", + "items": {"type": "string"}, + "title": "Cloudbase-Init Plugins", + "description": ( + "Custom Cloudbase-Init plugin class names for Windows guests. " + "This overrides the destination provider's list of " + "cloudbase-init plugins that get enabled after migrating " + "Windows guests." + ), +} + +CLOUDBASE_INIT_PLUGINS_DESTINATION_OPTION = { + "name": "cloudbase_init_plugins", + "values": [ + { + "id": item, + "name": item.rsplit(".", 1)[-1], + } + for item in CLOUDBASE_INIT_DEFAULT_PLUGINS + ], + "config_default": list(CLOUDBASE_INIT_DEFAULT_PLUGINS), +} + +CLOUDBASE_INIT_PLUGINS_OVERRIDE = { + "option": "cloudbase_init_plugins", + "field": "cloudbase_init_plugins", + "target": "parameters", +} + +# name: destination environment key. +# schema: JSON Schema fragment injected into the provider schema. +# destination_option: wizard row. Its name matches the key. +# override: where apply_core_destination_overrides stores the value. +# config: oslo option in COMMON_TARGET_OPTION_GROUP. +_OPTIONS = ( + { + "name": "cloudbase_init_plugins", + "schema": CLOUDBASE_INIT_PLUGINS_SCHEMA, + "destination_option": CLOUDBASE_INIT_PLUGINS_DESTINATION_OPTION, + "override": CLOUDBASE_INIT_PLUGINS_OVERRIDE, + "config": cfg.ListOpt( + "cloudbase_init_plugins", + default=None, + help=CLOUDBASE_INIT_PLUGINS_SCHEMA["description"], + ), + }, +) + +SCHEMA_PROPERTIES = {spec["name"]: spec["schema"] for spec in _OPTIONS} +DESTINATION_OPTIONS = {spec["name"]: spec["destination_option"] for spec in _OPTIONS} +OVERRIDES = tuple(spec["override"] for spec in _OPTIONS) + +opts = [spec["config"] for spec in _OPTIONS] + +CONF = cfg.CONF +CONF.register_opts(opts, COMMON_TARGET_OPTION_GROUP) + +LOG = logging.getLogger(__name__) + + +def _parse_cloudbase_init_plugins(plugins: list | None) -> list | None: + """Return the plugin list, or None if unset. + + None means the caller left the value unset. + Any other value must be a list. + """ + if plugins is None: + return None + if not isinstance(plugins, list): + raise exception.CoriolisException("Invalid plugins parameter. Must be list.") + return plugins + + +def resolve_cloudbase_init_plugins( + osmorphing_parameters: dict, plugins: list | None = None +) -> list: + """Return the Cloudbase-Init plugin list to write into the guest. + + ``osmorphing_parameters`` is a dict. ``plugins`` is a list or None. + + Resolved in this order: + + 1. osmorphing_parameters ``cloudbase_init_plugins``. + ``apply_core_destination_overrides`` stores the coriolis.conf + value here before the provider argument is read. + 2. provider-supplied ``plugins`` argument + 3. coriolis.conf ``[common_target_options] cloudbase_init_plugins`` + 4. ``CLOUDBASE_INIT_DEFAULT_PLUGINS`` + """ + if not isinstance(osmorphing_parameters, dict): + raise exception.CoriolisException( + "Invalid OS morphing parameters type: %s. Expected a dict." + % type(osmorphing_parameters).__name__ + ) + param_plugins = _parse_cloudbase_init_plugins( + osmorphing_parameters.get("cloudbase_init_plugins") + ) + if param_plugins is not None: + LOG.info( + "Using Cloudbase-Init plugins from OS morphing parameters: %s", + param_plugins, + ) + return param_plugins + + provider_plugins = _parse_cloudbase_init_plugins(plugins) + if provider_plugins is not None: + return provider_plugins + + conf_plugins = _parse_cloudbase_init_plugins( + CONF.common_target_options.cloudbase_init_plugins + ) + if conf_plugins is not None: + LOG.info("Using Cloudbase-Init plugins from coriolis.conf: %s", conf_plugins) + return conf_plugins + + return list(CLOUDBASE_INIT_DEFAULT_PLUGINS) diff --git a/coriolis/osmorphing/windows.py b/coriolis/osmorphing/windows.py index bd34fc277..c275643d5 100644 --- a/coriolis/osmorphing/windows.py +++ b/coriolis/osmorphing/windows.py @@ -14,6 +14,7 @@ from coriolis import constants, exception, utils from coriolis.osmorphing import base +from coriolis.osmorphing import conf as osmorphing_conf from coriolis.osmorphing.osdetect import windows as windows_osdetect LOG = logging.getLogger(__name__) @@ -30,21 +31,6 @@ RUN_PATH_FORMAT = "HKLM:\\%s\\\Microsoft\\Windows\\CurrentVersion\\Run" UNINSTALL_PATH_FORMAT = "HKLM:\\%s\\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*" CLOUDBASEINIT_SERVICE_NAME = "cloudbase-init" -CLOUDBASE_INIT_DEFAULT_PLUGINS = [ - 'cloudbaseinit.plugins.common.mtu.MTUPlugin', - 'cloudbaseinit.plugins.windows.ntpclient.NTPClientPlugin', - 'cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin', - 'cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin', - 'cloudbaseinit.plugins.windows.licensing.WindowsLicensingPlugin', - 'cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin', - 'cloudbaseinit.plugins.common.userdata.UserDataPlugin', - 'cloudbaseinit.plugins.common.setuserpassword.SetUserPasswordPlugin', - 'cloudbaseinit.plugins.windows.winrmlistener.ConfigWinRMListenerPlugin', - 'cloudbaseinit.plugins.windows.winrmcertificateauth.' - 'ConfigWinRMCertificateAuthPlugin', - 'cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin', -] - CLOUDBASE_INIT_DEFAULT_METADATA_SVCS = [ 'cloudbaseinit.metadata.services.httpservice.HttpService', 'cloudbaseinit.metadata.services.configdrive.ConfigDriveService', @@ -543,12 +529,9 @@ def _write_cloudbase_init_conf( if metadata_services is None: metadata_services = CLOUDBASE_INIT_DEFAULT_METADATA_SVCS - if plugins is None: - plugins = CLOUDBASE_INIT_DEFAULT_PLUGINS - elif type(plugins) is not list: - raise exception.CoriolisException( - "Invalid plugins parameter. Must be list." - ) + plugins = osmorphing_conf.resolve_cloudbase_init_plugins( + self._osmorphing_parameters, plugins + ) LOG.info("Writing Cloudbase-Init configuration files") conf_dir = "%s\\conf" % cloudbaseinit_base_dir diff --git a/coriolis/providers/conf.py b/coriolis/providers/conf.py new file mode 100644 index 000000000..2163428d8 --- /dev/null +++ b/coriolis/providers/conf.py @@ -0,0 +1,317 @@ +# Copyright 2026 Cloudbase Solutions Srl +# All Rights Reserved. + +"""Provider-agnostic destination options registered with oslo.config. + +Core dest-options are a list of wizard rows. The worker merges the +provider list onto that catalog with jsonmerge arrayMergeById on name. +The provider overwrites matching fields. Core rows always stay. +Provider-written schema properties are kept. +Destination environment values take precedence over provider values. +Provider values take precedence over coriolis.conf. +The oslo options are in the common_target_options group. +""" + +import copy + +from jsonmerge import Merger +from oslo_config import cfg +from oslo_log import log as logging + +from coriolis import exception +from coriolis.osmorphing import conf as osmorphing_conf + +COMMON_TARGET_OPTION_GROUP = osmorphing_conf.COMMON_TARGET_OPTION_GROUP + +CORE_SCHEMA_PROPERTIES = { + **osmorphing_conf.SCHEMA_PROPERTIES, + "data_transfer_mechanism": { + "type": "string", + "title": "Data Transfer Mechanism", + "enum": ["SSH", "HTTPS"], + "description": ( + "What mechanism to use when sending disk data from the " + "Coriolis installation to the temporary VMs on the " + "destination to be written to their respective disk. " + "The HTTPS-based transfer mechanism (TCP/5566) is faster " + "but might not work if there are firewalls in the way. " + "The SSH-based transfer mechanism (TCP/22) is more costly " + "but will be allowed by most firewalls since SSH access " + "from the Coriolis installation to the temporary worker " + "VM is always required. Default is HTTPS." + ), + }, + "set_dhcp": { + "type": "boolean", + "title": "Set DHCP", + "description": ( + "Sets whether or not to configure the VM to use DHCP " + "during the OSMorphing stage. " + "When disabled, guest static IPs will be preserved " + "after the migration." + ), + }, + "retain_user_credentials": { + "type": "boolean", + "title": "Retain User Credentials", + "description": ( + "Prevents the destination cloud provisioning from resetting " + "the pre-existing credentials. " + "When disabled, the destination platform may also create " + "a provisioned cloud guest user." + ), + }, +} + +CORE_DESTINATION_OPTIONS = { + **osmorphing_conf.DESTINATION_OPTIONS, + "data_transfer_mechanism": { + "name": "data_transfer_mechanism", + "values": ["SSH", "HTTPS"], + "config_default": "HTTPS", + }, + "set_dhcp": { + "name": "set_dhcp", + "values": [], + "config_default": True, + }, + "retain_user_credentials": { + "name": "retain_user_credentials", + "values": [], + "config_default": False, + }, +} + +# option: name in the destination environment and in coriolis.conf. +# field: stored name. It can differ from option. +# target "parameters": write the field on osmorphing_parameters. +# target "info": write the field on osmorphing_info. +OSMORPHING_OVERRIDES = ( + *osmorphing_conf.OVERRIDES, + { + "option": "set_dhcp", + "field": "nics_set_dhcp", + "target": "info", + }, + { + "option": "retain_user_credentials", + "field": "retain_user_credentials", + "target": "parameters", + }, +) + +_DEST_OPTIONS_MERGER = Merger( + { + "mergeStrategy": "arrayMergeById", + "mergeOptions": {"idRef": "/name"}, + "items": { + "type": "object", + "mergeStrategy": "objectMerge", + }, + } +) + +opts = [ + cfg.BoolOpt( + "set_dhcp", + default=CORE_DESTINATION_OPTIONS["set_dhcp"]["config_default"], + help=CORE_SCHEMA_PROPERTIES["set_dhcp"]["description"], + ), + cfg.BoolOpt( + "retain_user_credentials", + default=CORE_DESTINATION_OPTIONS["retain_user_credentials"]["config_default"], + help=CORE_SCHEMA_PROPERTIES["retain_user_credentials"]["description"], + ), +] + +CONF = cfg.CONF +CONF.register_opts(opts, COMMON_TARGET_OPTION_GROUP) + +LOG = logging.getLogger(__name__) + + +def _requested_core_names(option_names: list | dict | None) -> set | None: + """Return the requested core option names. + + ``None``, ``{}``, and ``[]`` request every destination option. + A non-empty list requests those names. + Any other type raises before the destination provider runs. + """ + if option_names is None or option_names == {}: + return None + if not isinstance(option_names, list): + raise exception.InvalidInput( + "Invalid destination option names type: %s. Expected a list." + % type(option_names).__name__ + ) + if not option_names: + return None + return set(option_names) + + +def merge_core_destination_options( + provider_options: list, + option_names: list | dict | None = None, +) -> list: + """Merge provider dest-options onto the core list. + + Use jsonmerge arrayMergeById on name. Core is the base. The + provider list is the head. Matching rows merge field by field. + Provider-only rows are appended. Core rows always stay. + ``provider_options`` is the list returned by the provider. + ``option_names`` follows ``_requested_core_names``. + """ + requested = _requested_core_names(option_names) + core = [ + copy.deepcopy(row) + for name, row in CORE_DESTINATION_OPTIONS.items() + if requested is None or name in requested + ] + LOG.info( + "Destination options before merge: core=%s provider=%s", core, provider_options + ) + merged = _DEST_OPTIONS_MERGER.merge(core, provider_options) + LOG.info("Destination options after merge: %s", merged) + return merged + + +def _inject_core_schema_property( + object_schema: dict, name: str, schema_fragment: dict +) -> None: + if "properties" not in object_schema: + return + props = object_schema["properties"] + if name in props: + return + props[name] = copy.deepcopy(schema_fragment) + + +def inject_core_target_environment_schema(schema: dict) -> dict: + """Write core destination fields onto a provider schema. + + The provider schema is a dict. ``oneOf`` and ``anyOf`` are lists of + object schemas when present. Skip a property the provider already + declared. Skip an object that has no ``properties`` field. + """ + schema = copy.deepcopy(schema) + for name, fragment in CORE_SCHEMA_PROPERTIES.items(): + _inject_core_schema_property(schema, name, fragment) + for key in ("oneOf", "anyOf"): + for alt in schema.get(key, ()): + _inject_core_schema_property(alt, name, fragment) + return schema + + +def filter_core_option_names( + option_names: list | dict | None, +) -> list | dict | None: + """Remove injected options so destination providers do not reject them. + + ``None`` and ``{}`` request every destination option and are returned + unchanged. A list returns the same names with core names removed. + An empty result means the request named only core options. + Any other type raises. + """ + if option_names is None or option_names == {}: + return option_names + if not isinstance(option_names, list): + raise exception.InvalidInput( + "Invalid destination option names type: %s. Expected a list." + % type(option_names).__name__ + ) + return [name for name in option_names if name not in CORE_DESTINATION_OPTIONS] + + +def _current_values(spec: dict, osmorphing_info: dict, params: dict) -> dict: + if spec["target"] == "parameters": + return params + return osmorphing_info + + +def _resolve_destination_option( + spec: dict, dest_env: dict, current: dict +) -> bool | list | None: + """Return the stored value for one destination option. + + Resolved in this order: + + 1. The destination environment value when it is not None. + 2. The current stored value when the destination environment + omits the key. + 3. The common_target_options value in coriolis.conf. + + False and [] are stored. None means the option stays unset. + """ + option = spec["option"] + field = spec["field"] + if option in dest_env and dest_env[option] is not None: + return dest_env[option] + if field in current and current[field] is not None: + return current[field] + group = getattr(CONF, COMMON_TARGET_OPTION_GROUP) + return getattr(group, option, None) + + +def apply_core_destination_overrides( + osmorphing_info: dict, + target_environment: dict, +) -> dict: + """Copy destination environment options onto osmorphing info. + + ``osmorphing_info`` and ``target_environment`` are dicts. + ``osmorphing_parameters`` is a dict when the key is present. + A missing ``osmorphing_parameters`` key stays an empty dict. + Each entry in ``OSMORPHING_OVERRIDES`` uses + ``_resolve_destination_option``. + """ + if not isinstance(osmorphing_info, dict): + raise exception.InvalidInput( + "Invalid osmorphing info type: %s. Expected a dict." + % type(osmorphing_info).__name__ + ) + dest_env = target_environment + + osmorphing_info = dict(osmorphing_info) + if "osmorphing_parameters" in osmorphing_info: + raw_params = osmorphing_info["osmorphing_parameters"] + if not isinstance(raw_params, dict): + raise exception.InvalidInput( + "Invalid osmorphing parameters type: %s. Expected a dict." + % type(raw_params).__name__ + ) + params = dict(raw_params) + else: + params = {} + option_names = [spec["option"] for spec in OSMORPHING_OVERRIDES] + LOG.debug( + "Destination options before apply: dest-env=%s params=%s info=%s", + {name: dest_env[name] for name in option_names if name in dest_env}, + {name: params[name] for name in option_names if name in params}, + { + spec["field"]: osmorphing_info[spec["field"]] + for spec in OSMORPHING_OVERRIDES + if spec["target"] == "info" and spec["field"] in osmorphing_info + }, + ) + + for spec in OSMORPHING_OVERRIDES: + if spec["target"] != "parameters": + params.pop(spec["option"], None) + current = _current_values(spec, osmorphing_info, params) + value = _resolve_destination_option(spec, dest_env, current) + if value is None: + continue + current[spec["field"]] = value + LOG.info("Applying destination option '%s': %s", spec["field"], value) + + osmorphing_info["osmorphing_parameters"] = params + LOG.debug( + "Destination options after apply: params=%s info=%s", + {name: params[name] for name in option_names if name in params}, + { + spec["field"]: osmorphing_info[spec["field"]] + for spec in OSMORPHING_OVERRIDES + if spec["target"] == "info" and spec["field"] in osmorphing_info + }, + ) + return osmorphing_info diff --git a/coriolis/schemas/os_morphing_resources_schema.json b/coriolis/schemas/os_morphing_resources_schema.json index 7591a8aa0..ae4c621b0 100644 --- a/coriolis/schemas/os_morphing_resources_schema.json +++ b/coriolis/schemas/os_morphing_resources_schema.json @@ -43,7 +43,8 @@ }, "nics_set_dhcp": { "type": "boolean", - "default": "true" + "default": true, + "description": "Sets whether or not to configure the VM to use DHCP during the OSMorphing stage. When disabled, guest static IPs will be preserved after the migration." }, "ignore_devices": { "type": "array", @@ -56,7 +57,15 @@ "properties": { "retain_user_credentials": { "type": "boolean", - "default": false + "default": false, + "description": "Prevents the destination cloud provisioning from resetting the pre-existing credentials. When disabled, the destination platform may also create a provisioned cloud guest user." + }, + "cloudbase_init_plugins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Custom Cloudbase-Init plugin class names for Windows guests. This overrides the destination provider's list of cloudbase-init plugins that get enabled after migrating Windows guests." } } } diff --git a/coriolis/tasks/osmorphing_tasks.py b/coriolis/tasks/osmorphing_tasks.py index b8afd8965..785240db0 100644 --- a/coriolis/tasks/osmorphing_tasks.py +++ b/coriolis/tasks/osmorphing_tasks.py @@ -5,6 +5,7 @@ from coriolis import constants, exception, schemas from coriolis.osmorphing import manager as osmorphing_manager +from coriolis.providers import conf as provider_conf from coriolis.providers import factory as providers_factory from coriolis.tasks import base @@ -150,6 +151,11 @@ def _run(self, ctxt, instance, origin, destination, task_info, event_handler): destination["type"], os_morphing_info, ) + os_morphing_info = {} + + os_morphing_info = provider_conf.apply_core_destination_overrides( + os_morphing_info, target_environment + ) return { "os_morphing_resources": os_morphing_resources, diff --git a/coriolis/tests/osmorphing/test_manager.py b/coriolis/tests/osmorphing/test_manager.py index 604e0b4f8..ef908baf8 100644 --- a/coriolis/tests/osmorphing/test_manager.py +++ b/coriolis/tests/osmorphing/test_manager.py @@ -7,6 +7,7 @@ from coriolis import constants, exception from coriolis.osmorphing import base as base_osmorphing from coriolis.osmorphing import manager +from coriolis.providers import conf as provider_conf from coriolis.tests import test_base @@ -420,3 +421,364 @@ def test_morph_image_dismount_os_exception( self._mock_user_scripts, self.event_handler, ) + + def test_apply_core_destination_overrides_plugins_from_target_env(self): + osmorphing_info = { + "os_type": "windows", + "nics_set_dhcp": True, + } + target_environment = { + "cloudbase_init_plugins": ["cloudbaseinit.plugins.common.mtu.MTUPlugin"] + } + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, target_environment + ) + self.assertEqual( + result["osmorphing_parameters"]["cloudbase_init_plugins"], + target_environment["cloudbase_init_plugins"], + ) + self.assertTrue(result["nics_set_dhcp"]) + self.assertNotIn("set_dhcp", result["osmorphing_parameters"]) + self.assertNotIn( + "cloudbase_init_plugins", osmorphing_info.get("osmorphing_parameters") or {} + ) + + def test_apply_core_destination_overrides_plugins_empty_list(self): + osmorphing_info = {"os_type": "windows"} + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"cloudbase_init_plugins": []} + ) + self.assertEqual(result["osmorphing_parameters"]["cloudbase_init_plugins"], []) + + def test_apply_core_destination_overrides_plugins_keeps_provider(self): + plugins = ["cloudbaseinit.plugins.common.mtu.MTUPlugin"] + osmorphing_info = { + "os_type": "windows", + "osmorphing_parameters": {"cloudbase_init_plugins": plugins}, + } + provider_conf.CONF.set_override( + "cloudbase_init_plugins", + ["other.Plugin"], + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.addCleanup( + provider_conf.CONF.clear_override, + "cloudbase_init_plugins", + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"zone": "zone1"} + ) + self.assertEqual( + result["osmorphing_parameters"]["cloudbase_init_plugins"], plugins + ) + + def test_apply_core_destination_overrides_rejects_non_dict(self): + self.assertRaises( + exception.InvalidInput, + provider_conf.apply_core_destination_overrides, + "not-a-dict", + {"set_dhcp": False}, + ) + self.assertRaises( + exception.InvalidInput, + provider_conf.apply_core_destination_overrides, + {"os_type": "windows", "osmorphing_parameters": "bad"}, + {"zone": "zone1"}, + ) + + def test_apply_core_destination_overrides_plugins_omitted(self): + osmorphing_info = {"os_type": "windows"} + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"zone": "zone1"} + ) + self.assertNotIn( + "cloudbase_init_plugins", result.get("osmorphing_parameters") or {} + ) + self.assertTrue(result["nics_set_dhcp"]) + self.assertNotIn("set_dhcp", result.get("osmorphing_parameters") or {}) + self.assertFalse(result["osmorphing_parameters"]["retain_user_credentials"]) + + def test_inject_core_target_environment_schema_simple(self): + schema = { + "type": "object", + "properties": {"zone": {"type": "string"}}, + "additionalProperties": False, + } + result = provider_conf.inject_core_target_environment_schema(schema) + self.assertIn("cloudbase_init_plugins", result["properties"]) + self.assertIn("data_transfer_mechanism", result["properties"]) + self.assertIn("set_dhcp", result["properties"]) + self.assertIn("retain_user_credentials", result["properties"]) + self.assertEqual( + ["SSH", "HTTPS"], result["properties"]["data_transfer_mechanism"]["enum"] + ) + self.assertEqual("boolean", result["properties"]["set_dhcp"]["type"]) + self.assertEqual( + "boolean", result["properties"]["retain_user_credentials"]["type"] + ) + self.assertNotIn("cloudbase_init_plugins", schema["properties"]) + self.assertNotIn("data_transfer_mechanism", schema["properties"]) + self.assertNotIn("set_dhcp", schema["properties"]) + self.assertNotIn("retain_user_credentials", schema["properties"]) + + def test_inject_core_target_environment_schema_keeps_provider(self): + schema = { + "type": "object", + "properties": { + "set_dhcp": { + "type": "string", + "title": "provider", + }, + }, + } + result = provider_conf.inject_core_target_environment_schema(schema) + self.assertEqual("string", result["properties"]["set_dhcp"]["type"]) + self.assertEqual("provider", result["properties"]["set_dhcp"]["title"]) + self.assertIn("cloudbase_init_plugins", result["properties"]) + self.assertEqual("string", schema["properties"]["set_dhcp"]["type"]) + + def test_inject_core_target_environment_schema_oneof(self): + schema = { + "oneOf": [ + {"properties": {"migr_network": {"type": "string"}}}, + {"properties": {"network_map": {"type": "object"}}}, + ] + } + result = provider_conf.inject_core_target_environment_schema(schema) + for alt in result["oneOf"]: + self.assertIn("cloudbase_init_plugins", alt["properties"]) + self.assertIn("data_transfer_mechanism", alt["properties"]) + self.assertIn("set_dhcp", alt["properties"]) + self.assertIn("retain_user_credentials", alt["properties"]) + + def _row_by_name(self, options, name): + for opt in options: + if opt.get("name") == name: + return opt + self.fail("Missing destination option %s" % name) + + def test_merge_core_destination_options_appends(self): + options = [{"name": "zone", "values": []}] + result = provider_conf.merge_core_destination_options(options) + names = [opt["name"] for opt in result] + self.assertIn("cloudbase_init_plugins", names) + self.assertIn("data_transfer_mechanism", names) + self.assertIn("set_dhcp", names) + self.assertIn("retain_user_credentials", names) + self.assertEqual(options, [{"name": "zone", "values": []}]) + + def test_merge_core_destination_options_provider_overwrites(self): + provider_options = [ + { + "name": "cloudbase_init_plugins", + "values": ["already-set"], + } + ] + merged = provider_conf.merge_core_destination_options(provider_options) + self.assertEqual( + ["already-set"], + self._row_by_name(merged, "cloudbase_init_plugins")["values"], + ) + self.assertEqual( + "HTTPS", + self._row_by_name(merged, "data_transfer_mechanism")["config_default"], + ) + self.assertEqual(True, self._row_by_name(merged, "set_dhcp")["config_default"]) + self.assertEqual( + False, + self._row_by_name(merged, "retain_user_credentials")["config_default"], + ) + + def test_merge_core_destination_options_nested_row_fields(self): + provider_options = [ + { + "name": "set_dhcp", + "config_default": False, + } + ] + merged = provider_conf.merge_core_destination_options(provider_options) + row = self._row_by_name(merged, "set_dhcp") + self.assertFalse(row["config_default"]) + self.assertEqual([], row["values"]) + + def test_merge_dest_opts_list_respects_names(self): + options = [{"name": "zone", "values": []}] + result = provider_conf.merge_core_destination_options( + options, option_names=["zone"] + ) + self.assertEqual(options, result) + + def test_merge_dest_opts_list_empty_dict(self): + options = [{"name": "zone", "values": []}] + result = provider_conf.merge_core_destination_options(options, option_names={}) + names = [opt["name"] for opt in result] + self.assertIn("cloudbase_init_plugins", names) + self.assertIn("data_transfer_mechanism", names) + self.assertIn("set_dhcp", names) + self.assertIn("retain_user_credentials", names) + + def test_merge_core_names_only(self): + result = provider_conf.merge_core_destination_options( + [], + option_names=["set_dhcp", "retain_user_credentials"], + ) + self.assertEqual( + ["set_dhcp", "retain_user_credentials"], + [opt["name"] for opt in result], + ) + + def test_filter_core_option_names(self): + result = provider_conf.filter_core_option_names( + [ + "zone", + "cloudbase_init_plugins", + "data_transfer_mechanism", + "set_dhcp", + "retain_user_credentials", + "import_node", + ] + ) + self.assertEqual(["zone", "import_node"], result) + self.assertEqual({}, provider_conf.filter_core_option_names({})) + self.assertIsNone(provider_conf.filter_core_option_names(None)) + self.assertRaises( + exception.InvalidInput, + provider_conf.filter_core_option_names, + "zone", + ) + + def test_get_data_transfer_mechanism_destination_option(self): + result = provider_conf.CORE_DESTINATION_OPTIONS["data_transfer_mechanism"] + self.assertEqual("data_transfer_mechanism", result["name"]) + self.assertEqual(["SSH", "HTTPS"], result["values"]) + self.assertEqual("HTTPS", result["config_default"]) + + def test_get_cloudbase_init_plugins_destination_option(self): + result = provider_conf.CORE_DESTINATION_OPTIONS["cloudbase_init_plugins"] + self.assertEqual("cloudbase_init_plugins", result["name"]) + self.assertTrue(result["values"]) + self.assertIn("id", result["values"][0]) + self.assertIn("name", result["values"][0]) + self.assertEqual( + [item["id"] for item in result["values"]], result["config_default"] + ) + + def test_apply_core_destination_overrides_set_dhcp(self): + osmorphing_info = { + "os_type": "linux", + "osmorphing_parameters": {}, + } + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"set_dhcp": False} + ) + self.assertFalse(result["nics_set_dhcp"]) + self.assertNotIn("set_dhcp", result.get("osmorphing_parameters") or {}) + self.assertNotIn("nics_set_dhcp", osmorphing_info) + + def test_apply_core_destination_overrides_set_dhcp_conf_fallback(self): + osmorphing_info = { + "os_type": "linux", + "osmorphing_parameters": {}, + } + provider_conf.CONF.set_override( + "set_dhcp", + False, + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.addCleanup( + provider_conf.CONF.clear_override, + "set_dhcp", + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"zone": "zone1"} + ) + self.assertFalse(result["nics_set_dhcp"]) + self.assertNotIn("set_dhcp", result.get("osmorphing_parameters") or {}) + + def test_apply_core_destination_overrides_set_dhcp_keeps_provider(self): + osmorphing_info = { + "os_type": "linux", + "nics_set_dhcp": True, + } + provider_conf.CONF.set_override( + "set_dhcp", + False, + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.addCleanup( + provider_conf.CONF.clear_override, + "set_dhcp", + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"zone": "zone1"} + ) + self.assertTrue(result["nics_set_dhcp"]) + self.assertNotIn("set_dhcp", result.get("osmorphing_parameters") or {}) + + def test_apply_core_destination_overrides_set_dhcp_from_dest_env(self): + provider_conf.CONF.set_override( + "set_dhcp", + True, + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.addCleanup( + provider_conf.CONF.clear_override, + "set_dhcp", + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + result = provider_conf.apply_core_destination_overrides( + { + "os_type": "linux", + "nics_set_dhcp": True, + }, + {"set_dhcp": False}, + ) + self.assertFalse(result["nics_set_dhcp"]) + self.assertNotIn("set_dhcp", result.get("osmorphing_parameters") or {}) + + def test_apply_core_destination_overrides_retain_user_credentials(self): + osmorphing_info = { + "os_type": "linux", + "osmorphing_parameters": {}, + } + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"retain_user_credentials": True} + ) + self.assertTrue(result["osmorphing_parameters"]["retain_user_credentials"]) + + def test_apply_core_destination_overrides_retain_user_credentials_keeps_provider( + self, + ): + osmorphing_info = { + "os_type": "linux", + "osmorphing_parameters": {"retain_user_credentials": True}, + } + provider_conf.CONF.set_override( + "retain_user_credentials", + False, + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.addCleanup( + provider_conf.CONF.clear_override, + "retain_user_credentials", + group=provider_conf.COMMON_TARGET_OPTION_GROUP, + ) + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"zone": "zone1"} + ) + self.assertTrue(result["osmorphing_parameters"]["retain_user_credentials"]) + + def test_apply_core_destination_overrides_retain_user_credentials_from_dest_env( + self, + ): + osmorphing_info = { + "os_type": "linux", + "osmorphing_parameters": {"retain_user_credentials": True}, + } + result = provider_conf.apply_core_destination_overrides( + osmorphing_info, {"retain_user_credentials": False} + ) + self.assertFalse(result["osmorphing_parameters"]["retain_user_credentials"]) diff --git a/coriolis/tests/osmorphing/test_windows.py b/coriolis/tests/osmorphing/test_windows.py index 220eff560..61ddeb084 100644 --- a/coriolis/tests/osmorphing/test_windows.py +++ b/coriolis/tests/osmorphing/test_windows.py @@ -10,6 +10,7 @@ import ddt from coriolis import exception +from coriolis.osmorphing import conf as osmorphing_conf from coriolis.osmorphing import windows from coriolis.tests import test_base @@ -579,7 +580,7 @@ def test__write_cloudbase_init_conf( "metadata_services": ",".join( windows.CLOUDBASE_INIT_DEFAULT_METADATA_SVCS ), - "plugins": ",".join(windows.CLOUDBASE_INIT_DEFAULT_PLUGINS), + "plugins": ",".join(osmorphing_conf.CLOUDBASE_INIT_DEFAULT_PLUGINS), "real_time_clock_utc": False, } ) @@ -592,12 +593,105 @@ def test__write_cloudbase_init_conf( 'C:\\Cloudbase-Init', mocked_full_path, priority=10 ) + def test__resolve_cloudbase_init_plugins_provider_default(self): + provider_plugins = ['cloudbaseinit.plugins.common.mtu.MTUPlugin'] + result = osmorphing_conf.resolve_cloudbase_init_plugins( + self.osmorphing_parameters, provider_plugins + ) + self.assertEqual(result, provider_plugins) + + def test_get_cloudbase_init_plugins_destination_option(self): + result = osmorphing_conf.CLOUDBASE_INIT_PLUGINS_DESTINATION_OPTION + self.assertEqual("cloudbase_init_plugins", result["name"]) + self.assertTrue(result["values"]) + self.assertEqual( + result["values"][0]["id"].rsplit(".", 1)[-1], result["values"][0]["name"] + ) + self.assertEqual( + [item["id"] for item in result["values"]], result["config_default"] + ) + + def test__resolve_cloudbase_init_plugins_core_default(self): + result = osmorphing_conf.resolve_cloudbase_init_plugins( + self.osmorphing_parameters + ) + self.assertEqual(result, osmorphing_conf.CLOUDBASE_INIT_DEFAULT_PLUGINS) + + def test__resolve_cloudbase_init_plugins_provider_beats_conf(self): + conf_plugins = ['cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin'] + provider_plugins = ['cloudbaseinit.plugins.common.mtu.MTUPlugin'] + osmorphing_conf.CONF.set_override( + 'cloudbase_init_plugins', + conf_plugins, + group=osmorphing_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.addCleanup( + osmorphing_conf.CONF.clear_override, + 'cloudbase_init_plugins', + group=osmorphing_conf.COMMON_TARGET_OPTION_GROUP, + ) + result = osmorphing_conf.resolve_cloudbase_init_plugins( + self.osmorphing_parameters, provider_plugins + ) + self.assertEqual(result, provider_plugins) + + def test__resolve_cloudbase_init_plugins_conf_override(self): + conf_plugins = ['cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin'] + osmorphing_conf.CONF.set_override( + 'cloudbase_init_plugins', + conf_plugins, + group=osmorphing_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.addCleanup( + osmorphing_conf.CONF.clear_override, + 'cloudbase_init_plugins', + group=osmorphing_conf.COMMON_TARGET_OPTION_GROUP, + ) + result = osmorphing_conf.resolve_cloudbase_init_plugins( + self.osmorphing_parameters + ) + self.assertEqual(result, conf_plugins) + + def test__resolve_cloudbase_init_plugins_api_override(self): + api_plugins = ['cloudbaseinit.plugins.common.userdata.UserDataPlugin'] + osmorphing_conf.CONF.set_override( + 'cloudbase_init_plugins', + ['cloudbaseinit.plugins.common.mtu.MTUPlugin'], + group=osmorphing_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.addCleanup( + osmorphing_conf.CONF.clear_override, + 'cloudbase_init_plugins', + group=osmorphing_conf.COMMON_TARGET_OPTION_GROUP, + ) + self.morphing_tools._osmorphing_parameters = { + "cloudbase_init_plugins": api_plugins + } + result = osmorphing_conf.resolve_cloudbase_init_plugins( + self.morphing_tools._osmorphing_parameters, + ['ignored.provider.Plugin'], + ) + self.assertEqual(result, api_plugins) + + def test__resolve_cloudbase_init_plugins_rejects_string(self): + self.morphing_tools._osmorphing_parameters = { + "cloudbase_init_plugins": ( + "cloudbaseinit.plugins.common.mtu.MTUPlugin, " + "cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin" + ) + } + self.assertRaises( + exception.CoriolisException, + osmorphing_conf.resolve_cloudbase_init_plugins, + self.morphing_tools._osmorphing_parameters, + ) + @mock.patch.object(windows.utils, 'write_winrm_file') @mock.patch.object(windows.BaseWindowsMorphingTools, '_write_local_script') def test__write_cloudbase_init_conf_with_exception( self, mock_write_local_script, mock_write_winrm_file ): - plugins = "invalid plugins" + plugins = {"invalid": "plugins"} self.assertRaises( exception.CoriolisException, diff --git a/coriolis/tests/tasks/test_osmorphing_tasks.py b/coriolis/tests/tasks/test_osmorphing_tasks.py index 3b71f714a..e76c65e4e 100644 --- a/coriolis/tests/tasks/test_osmorphing_tasks.py +++ b/coriolis/tests/tasks/test_osmorphing_tasks.py @@ -6,6 +6,7 @@ import ddt from coriolis import constants, exception, schemas +from coriolis.providers import conf as provider_conf from coriolis.tasks import osmorphing_tasks from coriolis.tests import test_base @@ -98,7 +99,10 @@ def test__run( prov_fun = mock_get_provider.return_value.deploy_os_morphing_resources prov_fun.return_value = import_info destination = mock.MagicMock() - task_info = mock.MagicMock() + task_info = { + "target_environment": {}, + "instance_deployment_info": {}, + } method_args = [ mock.sentinel.ctxt, mock.sentinel.instance, @@ -126,7 +130,10 @@ def _get_result(*args): expected_result = { "os_morphing_resources": import_info.get('os_morphing_resources'), "osmorphing_connection_info": (mock_marshal_conn_info.return_value), - "osmorphing_info": import_info.get('osmorphing_info', {}), + "osmorphing_info": provider_conf.apply_core_destination_overrides( + dict(import_info.get('osmorphing_info') or {}), + {}, + ), } self.assertEqual(expected_result, _get_result(*method_args)) mock_get_provider.assert_called_once_with( @@ -150,6 +157,58 @@ def _get_result(*args): import_info.get('osmorphing_connection_info') ) + @mock.patch('coriolis.providers.factory.get_provider') + @mock.patch('coriolis.tasks.base.get_connection_info') + @mock.patch('coriolis.schemas.validate_value') + @mock.patch('coriolis.tasks.base.marshal_migr_conn_info') + def test__run_applies_cloudbase_init_plugins_from_target_environment( + self, + mock_marshal_conn_info, + mock_validate_value, + mock_get_conn_info, + mock_get_provider, + ): + plugins = ['cloudbaseinit.plugins.common.mtu.MTUPlugin'] + import_info = { + "os_morphing_resources": {"res1": "id1"}, + "osmorphing_connection_info": {"info1": "secret1"}, + "osmorphing_info": { + "os_type": "windows", + "nics_set_dhcp": True, + }, + } + prov_fun = mock_get_provider.return_value.deploy_os_morphing_resources + prov_fun.return_value = import_info + task_info = { + "target_environment": { + "cloudbase_init_plugins": plugins, + "set_dhcp": False, + }, + "instance_deployment_info": {}, + } + destination = mock.MagicMock() + + result = self.task_runner._run( + mock.sentinel.ctxt, + mock.sentinel.instance, + mock.sentinel.origin, + destination, + task_info, + mock.sentinel.event_handler, + ) + + self.assertEqual( + result["osmorphing_info"]["osmorphing_parameters"][ + "cloudbase_init_plugins" + ], + plugins, + ) + self.assertFalse(result["osmorphing_info"]["nics_set_dhcp"]) + self.assertNotIn( + "set_dhcp", + result["osmorphing_info"].get("osmorphing_parameters") or {}, + ) + class DeleteOSMorphingResourcesTaskTestCase(test_base.CoriolisBaseTestCase): def setUp(self): diff --git a/coriolis/tests/worker/rpc/test_server.py b/coriolis/tests/worker/rpc/test_server.py index 292c8a636..0a123427d 100644 --- a/coriolis/tests/worker/rpc/test_server.py +++ b/coriolis/tests/worker/rpc/test_server.py @@ -14,6 +14,7 @@ from coriolis.conductor.rpc import client as conductor_client from coriolis.conductor.rpc import utils as cond_rpc_utils from coriolis.minion_manager.rpc import client as minion_client +from coriolis.providers import conf as provider_conf from coriolis.providers import factory as providers_factory from coriolis.tasks import factory as task_runners_factory from coriolis.tests import test_base @@ -638,16 +639,24 @@ def test_get_endpoint_instance( def test_get_endpoint_destination_options( self, mock_get_provider, mock_get_secret, mock_validate ): + provider_options = [{"name": "zone", "values": []}] + mock_get_provider.return_value.get_target_environment_options.return_value = ( + provider_options + ) + def call_get_endpoint_destination_options(): return self.server.get_endpoint_destination_options( mock.sentinel.context, mock.sentinel.platform_name, mock.sentinel.connection_info, mock.sentinel.environment, - mock.sentinel.option_names, + {}, ) options = call_get_endpoint_destination_options() + expected = provider_conf.merge_core_destination_options( + provider_options, option_names={} + ) mock_get_provider.assert_called_once_with( mock.sentinel.platform_name, @@ -662,22 +671,48 @@ def call_get_endpoint_destination_options(): mock.sentinel.context, mock_get_secret.return_value, env=mock.sentinel.environment, - option_names=mock.sentinel.option_names, + option_names={}, ) mock_validate.assert_called_once_with( - mock_get_provider.return_value.get_target_environment_options.return_value, + expected, schemas.CORIOLIS_DESTINATION_ENVIRONMENT_OPTIONS_SCHEMA, ) - self.assertEqual( - options, - mock_get_provider.return_value.get_target_environment_options.return_value, - ) + self.assertEqual(options, expected) # if the provider is not found, raise InvalidInput mock_get_provider.return_value = None self.assertRaises(exception.InvalidInput, call_get_endpoint_destination_options) + @mock.patch.object(schemas, "validate_value") + @mock.patch.object(utils, "get_secret_connection_info") + @mock.patch.object(providers_factory, "get_provider") + def test_get_endpoint_destination_options_core_names_only( + self, mock_get_provider, mock_get_secret, mock_validate + ): + option_names = ["set_dhcp", "retain_user_credentials"] + options = self.server.get_endpoint_destination_options( + mock.sentinel.context, + mock.sentinel.platform_name, + mock.sentinel.connection_info, + mock.sentinel.environment, + option_names, + ) + expected = provider_conf.merge_core_destination_options( + [], option_names=option_names + ) + + mock_get_secret.assert_not_called() + mock_get_provider.return_value.get_target_environment_options.assert_not_called() + mock_validate.assert_called_once_with( + expected, + schemas.CORIOLIS_DESTINATION_ENVIRONMENT_OPTIONS_SCHEMA, + ) + self.assertEqual( + ["set_dhcp", "retain_user_credentials"], + [opt["name"] for opt in options], + ) + @mock.patch.object(schemas, "validate_value") @mock.patch.object(utils, "get_secret_connection_info") @mock.patch.object(providers_factory, "get_provider") @@ -1008,6 +1043,10 @@ def test_validate_endpoint_source_environment( def test_validate_endpoint_target_environment( self, mock_get_provider, mock_validate ): + schema = {"type": "object", "properties": {"zone": {"type": "string"}}} + mock_get_provider.return_value.get_target_environment_schema.return_value = ( + schema + ) result = self.server.validate_endpoint_target_environment( mock.sentinel.context, mock.sentinel.target_platform_name, @@ -1021,7 +1060,7 @@ def test_validate_endpoint_target_environment( ) mock_validate.assert_called_once_with( mock.sentinel.target_environment, - mock_get_provider.return_value.get_target_environment_schema.return_value, + provider_conf.inject_core_target_environment_schema(schema), ) self.assertEqual(result, (True, None)) @@ -1167,6 +1206,10 @@ def call_validate_endpoint_connection(): ), ) def test_get_provider_schemas(self, schema_type, mock_get_provider): + mock_get_provider.return_value.get_target_environment_schema.return_value = { + "type": "object", + "properties": {}, + } provider_schemas = self.server.get_provider_schemas( mock.sentinel.context, mock.sentinel.platform_name, schema_type[0] ) diff --git a/coriolis/worker/rpc/server.py b/coriolis/worker/rpc/server.py index a7e0230ee..a47ecdd43 100644 --- a/coriolis/worker/rpc/server.py +++ b/coriolis/worker/rpc/server.py @@ -19,6 +19,7 @@ from coriolis.conductor.rpc import client as rpc_conductor_client from coriolis.conductor.rpc import utils as conductor_rpc_utils from coriolis.minion_manager.rpc import client as rpc_minion_manager_client +from coriolis.providers import conf as provider_conf from coriolis.providers import factory as providers_factory from coriolis.tasks import factory as task_runners_factory @@ -456,10 +457,27 @@ def get_endpoint_destination_options( "destination environment options." % platform_name ) - secret_connection_info = utils.get_secret_connection_info(ctxt, connection_info) - - options = provider.get_target_environment_options( - ctxt, secret_connection_info, env=env, option_names=option_names + provider_option_names = provider_conf.filter_core_option_names(option_names) + # Providers treat [] as every option. A request that names only + # core options must not be sent that way. + if ( + isinstance(option_names, list) + and option_names + and provider_option_names == [] + ): + options = [] + else: + secret_connection_info = utils.get_secret_connection_info( + ctxt, connection_info + ) + options = provider.get_target_environment_options( + ctxt, + secret_connection_info, + env=env, + option_names=provider_option_names, + ) + options = provider_conf.merge_core_destination_options( + options, option_names=option_names ) schemas.validate_value( @@ -604,6 +622,9 @@ def validate_endpoint_target_environment(self, ctxt, platform_name, target_env): platform_name, constants.PROVIDER_TYPE_OS_MORPHING, None ) target_env_schema = provider.get_target_environment_schema() + target_env_schema = provider_conf.inject_core_target_environment_schema( + target_env_schema + ) is_valid = True message = None @@ -718,6 +739,7 @@ def get_provider_schemas(self, ctxt, platform_name, provider_type): if provider_type == constants.PROVIDER_TYPE_TRANSFER_IMPORT: schema = provider.get_target_environment_schema() + schema = provider_conf.inject_core_target_environment_schema(schema) schemas["destination_environment_schema"] = schema if provider_type == constants.PROVIDER_TYPE_TRANSFER_EXPORT: diff --git a/etc/coriolis/coriolis.conf b/etc/coriolis/coriolis.conf index 5d172f54d..ea94d74ca 100644 --- a/etc/coriolis/coriolis.conf +++ b/etc/coriolis/coriolis.conf @@ -3,6 +3,24 @@ log_dir=/tmp log_file=coriolis.log messaging_transport_url=rabbit://coriolis:Passw0rd@127.0.0.1:5672/ +[common_target_options] +# Custom Cloudbase-Init plugin class names for Windows guests. +# This overrides the destination provider's list of cloudbase-init plugins +# that get enabled after migrating Windows guests. +# cloudbase_init_plugins = cloudbaseinit.plugins.common.mtu.MTUPlugin,cloudbaseinit.plugins.windows.ntpclient.NTPClientPlugin +# Configure guest NICs with DHCP during OS morphing. +# Written to osmorphing_info nics_set_dhcp. +# Used when the destination environment omits set_dhcp and the provider omits nics_set_dhcp. +# When disabled, guest static IPs are preserved after the migration. +# Default is true. +# set_dhcp = true +# Prevents the destination cloud provisioning from resetting the pre-existing credentials. +# When disabled, the destination platform may also create a provisioned cloud guest user. +# Stored on osmorphing_parameters retain_user_credentials. +# Used when the destination environment and the provider both omit the key. +# Default is false. +# retain_user_credentials = false + [keystone_authtoken] auth_type = password auth_url = http://keystone:35357 diff --git a/requirements.txt b/requirements.txt index 89d38ee77..b321dbbe6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ keystoneauth1 keystonemiddleware Jinja2 jsonschema +jsonmerge # NOTE (aznashwan, 21-03-31): kombu>=5 has some weird interactions with # oslo_messaging which causes extreme RAM usage in the API service, # so we limit its version here.