def setup(self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config.yml' self.config = MigrationConfig()
def main(): """ DistMigration post mount actions Preserve custom data file(s) e.g udev rules from the system to be migrated to the live migration system and activate those file changes to become effective """ Logger.setup() log = logging.getLogger(Defaults.get_migration_log_name()) root_path = Defaults.get_system_root_path() migration_config = MigrationConfig() preserve_info = migration_config.get_preserve_info() if preserve_info: for _, preserve_files in preserve_info.items(): for preserve_file in preserve_files: target_dir = os.path.dirname(preserve_file) source_file = os.path.normpath( os.sep.join([root_path, preserve_file])) log.info('Copy file: {0} to: {1}'.format( source_file, target_dir)) if not os.path.exists(target_dir): Command.run(['mkdir', '-p', target_dir]) shutil.copy(source_file, target_dir) if 'rules' in preserve_info.keys(): Command.run(['udevadm', 'control', '--reload']) Command.run( ['udevadm', 'trigger', '--type=subsystems', '--action=add']) Command.run( ['udevadm', 'trigger', '--type=devices', '--action=add'])
def main(): """ DistMigration post mount actions Preserve custom data file(s) e.g udev rules from the system to be migrated to the live migration system and activate those file changes to become effective """ Logger.setup() log = logging.getLogger(Defaults.get_migration_log_name()) root_path = Defaults.get_system_root_path() migration_config = MigrationConfig() system_udev_rules = migration_config.get_preserve_udev_rules_list() if system_udev_rules: for rule_file in system_udev_rules: target_rule_dir = os.path.dirname(rule_file) source_rule_file = os.path.normpath( os.sep.join([root_path, rule_file])) log.info('Copy udev rule: {0} to: {1}'.format( source_rule_file, target_rule_dir)) shutil.copy(source_rule_file, target_rule_dir) Command.run(['udevadm', 'control', '--reload']) Command.run( ['udevadm', 'trigger', '--type=subsystems', '--action=add']) Command.run(['udevadm', 'trigger', '--type=devices', '--action=add'])
def test_update_migration_config_file_violates_schema( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file, mock_write_config_file, ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config-violates-schema.yml' self.config = MigrationConfig() with raises(DistMigrationConfigDataException): self.config.update_migration_config_file()
def test_update_migration_config_file_very_broken( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file, mock_write_config_file, ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config-corrupt-mess.yml' self.config = MigrationConfig() with raises(DistMigrationConfigDataException): self.config.update_migration_config_file()
def test_update_migration_config_file_just_comments( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file, mock_write_config_file, ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config-just-comments.yml' self.config = MigrationConfig() self.config.update_migration_config_file()
def main(): """ DistMigration reboot with new kernel After the migration process is finished, the system reboots unless the debug option is set. Before reboot a reverse umount of the filesystems that got mounted by the mount_system service is performed and thus releases the upgraded system from the migration host. If for whatever reason a filesystem is busy and can't be umounted, this condition is not handled as an error. The reason is that the cleanup should not prevent us from continuing with the reboot process. The risk on reboot of the migration host with a potential active mount is something we accept """ Logger.setup() log = logging.getLogger(Defaults.get_migration_log_name()) try: log.info('Systemctl Status Information: {0}{1}'.format( os.linesep, Command.run(['systemctl', 'status', '-l', '--all'], raise_on_error=False).output)) # stop console dialog log. The service holds a busy state # on system-root and stands in our way in case of debug # mode because it grabs the master console in/output Command.run(['systemctl', 'stop', 'suse-migration-console-log'], raise_on_error=False) if MigrationConfig().is_debug_requested(): log.info('Reboot skipped due to debug flag set') else: log.info('Umounting system') system_mount = Fstab() system_mount.read(Defaults.get_system_mount_info_file()) for mount in reversed(system_mount.get_devices()): log.info('Umounting {0}: {1}'.format( mount.mountpoint, Command.run(['umount', '--lazy', mount.mountpoint], raise_on_error=False))) if not MigrationConfig().is_soft_reboot_requested(): restart_system = 'reboot' else: restart_system = 'kexec' log.info('Reboot system: {0}{1}'.format( os.linesep, Command.run(['systemctl', restart_system]))) except Exception: # Uhh, we don't want to be here, but we also don't # want to be stuck in the migration live system. # Keep fingers crossed: log.warning('Reboot system: [Force Reboot]') Command.run(['systemctl', '--force', 'reboot'])
def main(): """ DistMigration mount system to upgrade Searches on all partitions for a fstab file. The first fstab file found is used as the system to upgrade. Filesystems relevant for an upgrade process are read from that fstab in order and mounted such that the system rootfs is available for a zypper based migration process. """ Logger.setup() log = logging.getLogger(Defaults.get_migration_log_name()) root_path = Defaults.get_system_root_path() Path.create(root_path) log.info('Running mount system service') if is_mounted(root_path): # root_path is already a mount point, better not continue # The condition is not handled as an error because the # existing mount point under this service created root_path # is considered to represent the system to upgrade and # not something else. Thus if already mounted, let's use # what is there. return log.info('Mount system service: {0} is mounted'.format(root_path)) # Check if booted via loopback grub isoscan_loop_mount = '/run/initramfs/isoscan' if is_mounted(isoscan_loop_mount): # The system to become migrated was booted via a grub # loopback menuentry. This means the disk is blocked by # that readonly loopback mount and needs to be # remounted for read write access first log.info( 'Mount system service: {0} is mounted'.format(isoscan_loop_mount)) Command.run(['mount', '-o', 'remount,rw', isoscan_loop_mount]) fstab, storage_info = read_system_fstab(root_path) if not fstab: log.error('Could not find system in fstab on {0}'.format(storage_info)) raise DistMigrationSystemNotFoundException( 'Could not find system with fstab on {0}'.format(storage_info)) mount_system(root_path, fstab) migration_config = MigrationConfig() migration_config.update_migration_config_file() log.info('Config file content:\n{content}\n'.format( content=migration_config.get_migration_config_file_content()))
def main(): """ DistMigration run zypper based migration Call zypper migration plugin and migrate the system. The output of the call is logged on the system to migrate """ root_path = Defaults.get_system_root_path() try: log.info('Running migrate service') migration_config = MigrationConfig() if migration_config.is_zypper_migration_plugin_requested(): bash_command = ' '.join([ 'zypper', 'migration', '--non-interactive', '--gpg-auto-import-keys', '--no-selfupdate', '--auto-agree-with-licenses', '--allow-vendor-change', '--strict-errors-dist-migration', '--replacefiles', '--product', migration_config.get_migration_product(), '--root', root_path, '&>>', Defaults.get_migration_log_file() ]) Command.run(['bash', '-c', bash_command]) else: bash_command = ' '.join([ 'zypper', '--non-interactive', '--gpg-auto-import-keys', '--root', root_path, 'dup', '--auto-agree-with-licenses', '--allow-vendor-change', '--replacefiles', '&>>', Defaults.get_migration_log_file() ]) zypper_call = Command.run(['bash', '-c', bash_command], raise_on_error=False) if zypper_has_failed(zypper_call.returncode): raise DistMigrationCommandException( '{0} failed with: {1}: {2}'.format(bash_command, zypper_call.output, zypper_call.error)) except Exception as issue: etc_issue_path = os.sep.join([root_path, 'etc/issue']) log_path_migrated_system = os.sep + os.path.relpath( Defaults.get_migration_log_file(), root_path) with open(etc_issue_path, 'w') as issue_file: issue_file.write( 'Migration has failed, for further details see {0}'.format( log_path_migrated_system)) log.error('migrate service failed with {0}'.format(issue)) raise DistMigrationZypperException( 'Migration failed with {0}'.format(issue))
def main(): """ DistMigration load new kernel for kexec reboot Loads the new kernel/initrd after migration for system reboot """ Logger.setup() log = logging.getLogger(Defaults.get_migration_log_name()) if not MigrationConfig().is_soft_reboot_requested(): log.info('skipping kexec --load (hard reboot requested)') return root_path = Defaults.get_system_root_path() target_kernel = os.sep.join([root_path, Defaults.get_target_kernel()]) target_initrd = os.sep.join([root_path, Defaults.get_target_initrd()]) kexec_boot_data = '/var/tmp/kexec' Path.create(kexec_boot_data) shutil.copy(target_initrd, kexec_boot_data) try: log.info('Running kernel load service') log.info('Loading the target kernel') Command.run([ 'kexec', '--load', target_kernel, '--initrd', os.sep.join([kexec_boot_data, os.path.basename(target_initrd)]), '--kexec-file-syscall', '--command-line', _get_cmdline(os.path.basename(target_kernel)) ]) except Exception as issue: log.error('Kernel load service raised exception: {0}'.format(issue)) raise DistMigrationKernelRebootException( 'Failed to load kernel/initrd into memory: {0}'.format(issue))
def main(): """ DistMigration create a new initrd with added modules Run dracut to build a new initrd that includes multipath modules """ Logger.setup() log = logging.getLogger(Defaults.get_migration_log_name()) if MigrationConfig().is_host_independent_initd_requested(): log.info('Creating a new host independent initrd') root_path = Defaults.get_system_root_path() dracut_bind_mounts(root_path) run_dracut(root_path)
def main(): """ DistMigration prepare for migration Prepare the migration live system to allow zypper migration to upgrade the system across major distribution versions. The zypper migration process contacts the service that provides the configured repositories on the system being migrated. The service must be one of SUSE's repository services, SCC, RMT, or SMT. This requiers information from the target system. This service makes the necessary information available inside the live system that performs the migration. """ Logger.setup() log = logging.getLogger(Defaults.get_migration_log_name()) root_path = Defaults.get_system_root_path() suse_connect_setup = os.sep.join([root_path, 'etc', 'SUSEConnect']) suse_cloud_regionsrv_setup = os.sep.join( [root_path, 'etc', 'regionserverclnt.cfg']) hosts_setup = os.sep.join([root_path, 'etc', 'hosts']) trust_anchors = os.sep.join( [root_path, 'usr', 'share', 'pki', 'trust', 'anchors']) if os.path.exists(suse_connect_setup): shutil.copy(suse_connect_setup, '/etc/SUSEConnect') if os.path.exists(suse_cloud_regionsrv_setup): migration_suse_cloud_regionsrv_setup = '/etc/regionserverclnt.cfg' shutil.copy(suse_cloud_regionsrv_setup, migration_suse_cloud_regionsrv_setup) update_regionsrv_setup(root_path, migration_suse_cloud_regionsrv_setup) if os.path.exists(hosts_setup): shutil.copy(hosts_setup, '/etc/hosts') if os.path.exists(trust_anchors): certificates = os.listdir(trust_anchors) if certificates: for cert in certificates: log.info('Importing certificate: {0}'.format(cert)) shutil.copy(os.sep.join([trust_anchors, cert]), '/usr/share/pki/trust/anchors/') log.info('Update certificate pool') Command.run(['update-ca-certificates']) zypp_metadata = os.sep.join([root_path, 'etc', 'zypp']) zypp_plugins_services = os.sep.join( [root_path, 'usr', 'lib', 'zypp', 'plugins', 'services']) cloud_register_metadata = os.sep.join( [root_path, 'var', 'lib', 'cloudregister']) zypper_log_file = os.sep.join([root_path, 'var', 'log', 'zypper.log']) if os.path.exists(zypper_log_file): try: zypper_host_log_file = zypper_log_file.replace(root_path, '') if not os.path.exists(zypper_host_log_file): with open(zypper_host_log_file, 'w'): # we bind mount the system zypper log file # but the mount target does not exist. # Create it as empty file prior bind mounting pass Command.run( ['mount', '--bind', zypper_log_file, zypper_host_log_file]) except Exception as issue: log.warning( 'Bind mounting zypper log file failed with: {0}'.format(issue)) try: # log network info as network-online.target is done at this point log_network_details() log.info('Running prepare service') system_mount = Fstab() system_mount.read(Defaults.get_system_mount_info_file()) log.info('Bind mounting /etc/zypp') Command.run(['mount', '--bind', zypp_metadata, '/etc/zypp']) system_mount.add_entry(zypp_metadata, '/etc/zypp') log.info('Bind mounting /usr/lib/zypp/plugins') Command.run([ 'mount', '--bind', zypp_plugins_services, '/usr/lib/zypp/plugins/services' ]) system_mount.add_entry(zypp_plugins_services, '/usr/lib/zypp/plugins/services') if os.path.exists(cloud_register_metadata): log.info('Bind mounting /var/lib/cloudregister') Path.create('/var/lib/cloudregister') Command.run([ 'mount', '--bind', cloud_register_metadata, '/var/lib/cloudregister' ]) update_smt_cache = '/usr/sbin/updatesmtcache' if os.path.isfile(update_smt_cache): log.info('Updating SMT cache') Command.run([update_smt_cache]) system_mount.export(Defaults.get_system_mount_info_file()) # Check if system is registered migration_config = MigrationConfig() if migration_config.is_zypper_migration_plugin_requested(): if not SUSEConnect.is_registered(): message = 'System not registered. Aborting migration.' log.error(message) raise DistMigrationSystemNotRegisteredException(message) except Exception as issue: log.error( 'Preparation of zypper metadata failed with {0}'.format(issue)) # Not unmounting any of the bind mounts above; the reboot # service should take care of that anyway raise DistMigrationZypperMetaDataException( 'Preparation of zypper metadata failed with {0}'.format(issue))
class TestMigrationConfig(object): @patch.object(Defaults, 'get_migration_config_file') @patch.object(Defaults, 'get_system_migration_custom_config_file') def setup(self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config.yml' self.config = MigrationConfig() def test_get_migration_product(self): assert self.config.get_migration_product() == 'SLES/15/x86_64' def test_get_preserve_udev_rules_list(self): assert self.config.get_preserve_udev_rules_list() == [ '/etc/udev/rules.d/a.rules', '/etc/udev/rules.d/b.rules' ] @patch('suse_migration_services.logger.log.error') def test_get_migration_product_targets(self, mock_error): self.config.config_data = {'not_migration_product': 'another_info'} with raises(DistMigrationProductNotFoundException): self.config.get_migration_product() assert mock_error.called @patch.object(MigrationConfig, '_write_config_file') @patch('suse_migration_services.logger.log.info') def test_update_migration_config_file( self, mock_info, mock_write_config_file, ): self.config.update_migration_config_file() assert self.config.get_migration_product() == 'SLES/15.1/x86_64' assert self.config.is_debug_requested() is True assert mock_info.called def test_is_zypper_migration_plugin_requested(self): assert self.config.is_zypper_migration_plugin_requested() is True def test_is_debug_requested(self): assert self.config.is_debug_requested() is False @patch('yaml.dump') def test_write_config_file(self, mock_yaml_dump): with patch('builtins.open', create=True) as mock_open: mock_open.return_value = MagicMock(spec=io.IOBase) file_handle = mock_open.return_value.__enter__.return_value self.config._write_config_file() mock_open.assert_called_once_with( self.config.migration_config_file, 'w') mock_yaml_dump.assert_called_once_with(self.config.config_data, file_handle, default_flow_style=False)
class TestMigrationConfig(object): @patch.object(Defaults, 'get_migration_config_file') @patch.object(Defaults, 'get_system_migration_custom_config_file') def setup( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config.yml' self.config = MigrationConfig() @patch.object(Defaults, 'get_os_release') @patch.object(Defaults, 'get_system_root_path') def test_get_migration_product_auto_detected( self, mock_get_system_root_path, mock_get_os_release ): os_release_tuple = namedtuple( 'OSRelease', [ 'name', 'version', 'version_id', 'pretty_name', 'id', 'id_like', 'ansi_color', 'cpe_name' ] ) os_release_result = os_release_tuple( name='SLES', version='15-SP1', version_id='15.1', pretty_name='SUSE Linux Enterprise Server 15 SP1', id='sles', id_like='suse', ansi_color='0;32', cpe_name='cpe:/o:suse:sles:15:sp1' ) mock_get_system_root_path.return_value = '../data' mock_get_os_release.return_value = os_release_result assert self.config.get_migration_product() == 'SLES/15.1/x86_64' def test_get_preserve_udev_rules_list(self): assert self.config.get_preserve_udev_rules_list() == [ '/etc/udev/rules.d/a.rules', '/etc/udev/rules.d/b.rules' ] @patch.object(Defaults, 'get_os_release') @patch.object(SUSEBaseProduct, 'get_tag') @patch.object(Defaults, 'get_system_root_path') def test_get_migration_product_targets( self, mock_get_system_root_path, mock_get_product_name, mock_get_os_release ): os_release_tuple = namedtuple( 'OSRelease', [ 'name', 'version', 'version_id', 'pretty_name', 'id', 'id_like', 'ansi_color', 'cpe_name' ] ) os_release_result = os_release_tuple( name='SLES', version='15-SP1', version_id='15.1', pretty_name='SUSE Linux Enterprise Server 15 SP1', id='sles', id_like='suse', ansi_color='0;32', cpe_name='cpe:/o:suse:sles:15:sp1' ) mock_get_system_root_path.return_value = '../data' mock_get_os_release.return_value = os_release_result mock_get_product_name.side_effect = Exception self.config.config_data = {'not_migration_product': 'another_info'} with raises(DistMigrationProductNotFoundException): self.config.get_migration_product() @patch.object(Defaults, 'get_os_release') @patch.object(SUSEBaseProduct, 'get_product_name') @patch.object(Defaults, 'get_system_root_path') @patch.object(MigrationConfig, '_write_config_file') def test_update_migration_config_file_no_autodetect( self, mock_write_config_file, mock_get_system_root_path, mock_get_product_name, mock_get_os_release ): os_release_tuple = namedtuple( 'OSRelease', [ 'name', 'version', 'version_id', 'pretty_name', 'id', 'id_like', 'ansi_color', 'cpe_name' ] ) os_release_result = os_release_tuple( name='SLES', version='15-SP1', version_id='15.1', pretty_name='SUSE Linux Enterprise Server 15 SP1', id='sles', id_like='suse', ansi_color='0;32', cpe_name='cpe:/o:suse:sles:15:sp1' ) mock_get_os_release.return_value = os_release_result mock_get_system_root_path.return_value = '../data' mock_get_product_name.return_value = None self.config.update_migration_config_file() assert self.config.get_migration_product() == 'SLES/15.1/x86_64' assert self.config.is_debug_requested() is True def test_is_zypper_migration_plugin_requested(self): assert self.config.is_zypper_migration_plugin_requested() is True def test_is_debug_requested(self): assert self.config.is_debug_requested() is False @patch('yaml.dump') def test_write_config_file(self, mock_yaml_dump): with patch('builtins.open', create=True) as mock_open: mock_open.return_value = MagicMock(spec=io.IOBase) file_handle = mock_open.return_value.__enter__.return_value self.config._write_config_file() mock_open.assert_called_once_with( self.config.migration_config_file, 'w' ) mock_yaml_dump.assert_called_once_with( self.config.config_data, file_handle, default_flow_style=False ) @patch.object(MigrationConfig, '_write_config_file') @patch.object(Defaults, 'get_migration_config_file') @patch.object(Defaults, 'get_system_migration_custom_config_file') def test_update_migration_config_file_empty( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file, mock_write_config_file, ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config-empty.yml' self.config = MigrationConfig() self.config.update_migration_config_file() @patch.object(MigrationConfig, '_write_config_file') @patch.object(Defaults, 'get_migration_config_file') @patch.object(Defaults, 'get_system_migration_custom_config_file') def test_update_migration_config_file_just_comments( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file, mock_write_config_file, ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config-just-comments.yml' self.config = MigrationConfig() self.config.update_migration_config_file() @patch.object(MigrationConfig, '_write_config_file') @patch.object(Defaults, 'get_migration_config_file') @patch.object(Defaults, 'get_system_migration_custom_config_file') def test_update_migration_config_file_slightly_broken( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file, mock_write_config_file, ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config-corrupt-string.yml' self.config = MigrationConfig() with raises(DistMigrationConfigDataException): self.config.update_migration_config_file() @patch.object(MigrationConfig, '_write_config_file') @patch.object(Defaults, 'get_migration_config_file') @patch.object(Defaults, 'get_system_migration_custom_config_file') def test_update_migration_config_file_very_broken( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file, mock_write_config_file, ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config-corrupt-mess.yml' self.config = MigrationConfig() with raises(DistMigrationConfigDataException): self.config.update_migration_config_file() @patch.object(MigrationConfig, '_write_config_file') @patch.object(Defaults, 'get_migration_config_file') @patch.object(Defaults, 'get_system_migration_custom_config_file') def test_update_migration_config_file_violates_schema( self, mock_get_system_migration_config_custom_file, mock_get_migration_config_file, mock_write_config_file, ): mock_get_migration_config_file.return_value = \ '../data/migration-config.yml' mock_get_system_migration_config_custom_file.return_value = \ '../data/custom-migration-config-violates-schema.yml' self.config = MigrationConfig() with raises(DistMigrationConfigDataException): self.config.update_migration_config_file()
def main(): """ DistMigration reboot with new kernel After the migration process is finished, the system reboots unless the debug option is set. Before reboot a reverse umount of the filesystems that got mounted by the mount_system service is performed and thus releases the upgraded system from the migration host. If for whatever reason a filesystem is busy and can't be umounted, this condition is not handled as an error. The reason is that the cleanup should not prevent us from continuing with the reboot process. The risk on reboot of the migration host with a potential active mount is something we accept """ try: log.info( 'Systemctl Status Information: {0}{1}'.format( os.linesep, Command.run( ['systemctl', 'status', '-l', '--all'], raise_on_error=False ).output ) ) if MigrationConfig().is_debug_requested(): log.info('Reboot skipped due to debug flag set') else: log.info('Umounting system') system_mount = Fstab() system_mount.read( Defaults.get_system_mount_info_file() ) for mount in reversed(system_mount.get_devices()): log.info( 'Umounting {0}: {1}'.format( mount.mountpoint, Command.run( ['umount', '--lazy', mount.mountpoint], raise_on_error=False ) ) ) log.info( # reboot is performed through systemd. The call through # systemd checks if there is a kexec loaded kernel and # transparently turns 'systemctl reboot' into # 'systemctl kexec'. Thus both ways, soft and hard # reboot are managed in one call. 'Reboot system: {0}{1}'.format( os.linesep, Command.run( ['systemctl', 'reboot'] ) ) ) except Exception: # Uhh, we don't want to be here, but we also don't # want to be stuck in the migration live system. # Keep fingers crossed: log.warning('Reboot system: [Force Reboot]') Command.run( ['systemctl', '--force', 'reboot'] )