-
Couldn't load subscription status.
- Fork 179
add case for migration with ethernet interface #6625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| - virtual_network.migrate.migrate_with_ethernet_interface: | ||
| type = migrate_with_ethernet_interface | ||
| start_vm = "no" | ||
| take_regular_screendumps = no | ||
| storage_type = 'nfs' | ||
| setup_local_nfs = 'yes' | ||
| disk_type = "file" | ||
| disk_source_protocol = "netfs" | ||
| nfs_mount_dir = "/var/lib/libvirt/migrate" | ||
| nfs_mount_options = "soft,timeo=50,retrans=3" | ||
| virsh_migrate_dest_state = running | ||
| virsh_migrate_options = "--live --p2p --verbose" | ||
| outside_ip = "www.google.com" | ||
| migration_setup = "yes" | ||
| iface_model = "virtio" | ||
| ssh_remote_auth = True | ||
| unprivileged_user = "${test_user}" | ||
| mnt_path_name = ${nfs_mount_dir} | ||
| nfs_server_ip = "${migrate_source_host}" | ||
| migrate_desturi_port = "22" | ||
| migrate_desturi_type = "ssh" | ||
| virsh_migrate_desturi = "qemu+ssh://%s@${migrate_dest_host}/session" | ||
| vm_ping_outside = "pass" | ||
| check_network_accessibility_after_mig = "yes" | ||
|
|
||
| # Ethernet interface specific settings | ||
| bridge_name = "br0" | ||
| tap_name = "mytap0" | ||
| tap_mtu = "9000" | ||
| # Ethernet interface configuration with managed=no | ||
| iface_attrs = {'type_name': 'ethernet', 'target': {'dev': '${tap_name}', 'managed': 'no'}, 'model': '${iface_model}', 'mtu': {'size': '9000'}} | ||
| variants: | ||
| - precopy_migration: | ||
| migration_type = "precopy" | ||
| virsh_migrate_extra = "" | ||
| migrate_vm_back = "yes" | ||
| - postcopy_migration: | ||
| migration_type = "postcopy" | ||
| virsh_migrate_extra = "--postcopy --timeout 10 --timeout-postcopy --bandwidth 1" | ||
| migrate_vm_back = "yes" | ||
| - cancel_migration: | ||
| migration_type = "cancel" | ||
| cancel_migration = "yes" | ||
| status_error = "yes" | ||
| err_msg = "operation aborted: migration out job: canceled by client|operation aborted: job 'migration out' canceled by client" | ||
| virsh_migrate_extra = "--bandwidth 10" | ||
| action_during_mig = '[{"func": "virsh.domjobabort", "after_event": "iteration: '1'", "func_param": "'%s' % params.get('migrate_main_vm')"}]' | ||
|
|
||
| variants: | ||
| - unprivileged_user: | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| # | ||
| # Copyright Redhat | ||
| # | ||
| # SPDX-License-Identifier: GPL-2.0 | ||
| # Author: Nannan Li<[email protected]> | ||
| # | ||
| # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| import time | ||
|
|
||
| import aexpect.remote | ||
| from avocado.utils import process | ||
|
|
||
| from virttest import virsh | ||
| from virttest import utils_net | ||
| from virttest.libvirt_xml import vm_xml | ||
| from virttest.utils_libvirt import libvirt_vmxml | ||
| from virttest.utils_libvirt import libvirt_unprivileged | ||
|
|
||
| from provider.migration import base_steps | ||
| from provider.virtual_network import network_base | ||
|
|
||
|
|
||
| def run(test, params, env): | ||
| """ | ||
| Test migration with ethernet type interface managed=no | ||
|
|
||
| This case tests migration of vm with ethernet type interface to and back, | ||
| checking that network functions work well after migration. | ||
|
|
||
| Test matrix: | ||
| - precopy migration | ||
| - postcopy migration | ||
| - cancel migration | ||
|
|
||
| Setup: | ||
| 1. Prepare migration environment | ||
| 2. Prepare pre-created tap device and connect it to linux bridge on both hosts | ||
| 3. Start VM with ethernet type interface pointing to the tap device | ||
|
|
||
| :param test: test object | ||
| :param params: Dictionary with the test parameters | ||
| :param env: Dictionary with test environment. | ||
| """ | ||
| def create_unpr_user_and_vm(user_name, user_passwd, main_vm_name, remote_session): | ||
| """ | ||
| Create unprivileged user and setup VM for that user | ||
|
|
||
| :param user_name: unprivileged user name | ||
| :param user_passwd: unprivileged user password | ||
| :param main_vm_name: main vm name in env | ||
| :param remote_session: remote machine session | ||
| :return: VM object for unprivileged user | ||
| """ | ||
| test.log.info(f"Creating unprivileged user for local and remote: {user_name}") | ||
| try: | ||
| add_user, add_pwd = (f'useradd -m {user_name}', | ||
| f'echo "{user_name}:{user_passwd}" | chpasswd') | ||
| process.run(add_user, shell=True) | ||
| process.run(add_pwd, shell=True) | ||
| if remote_session: | ||
| remote_session.cmd(add_user) | ||
| remote_session.cmd(add_pwd) | ||
|
|
||
| except Exception as e: | ||
| test.log.warn(f"User {user_name} may already exist: {e}") | ||
|
|
||
| # Get original VM XML from root user | ||
| root_vmxml = vm_xml.VMXML.new_from_inactive_dumpxml(main_vm_name) | ||
|
|
||
| # Prepare VM XML for unprivileged user | ||
| test.log.info(f"Preparing VM XML for unprivileged user: {user_name}") | ||
| unpr_vmxml = network_base.prepare_vmxml_for_unprivileged_user(user_name, root_vmxml) | ||
|
|
||
| # Remove existing interfaces and add ethernet interface | ||
| unpr_vmxml.del_device('interface', by_tag=True) | ||
| libvirt_vmxml.modify_vm_device(unpr_vmxml, 'interface', iface_attrs) | ||
|
|
||
| # Remove DAC security label for unprivileged user | ||
| unpr_vmxml.del_seclabel(by_attr=[('model', 'dac')]) | ||
|
|
||
| # Define VM for unprivileged user | ||
| test.log.info(f"Defining VM for unprivileged user: {user_name}") | ||
| network_base.define_vm_for_unprivileged_user(user_name, unpr_vmxml) | ||
|
|
||
| # Get unprivileged VM object | ||
| unpr_vm_args = { | ||
| 'username': params.get('username'), | ||
| 'password': params.get('password'), | ||
| } | ||
| unpr_vm = libvirt_unprivileged.get_unprivileged_vm( | ||
| unpr_vmxml.vm_name, user_name, user_passwd, **unpr_vm_args) | ||
|
|
||
| test.log.info(f"Successfully created unprivileged VM: {unpr_vmxml.vm_name}") | ||
| return unpr_vm | ||
|
|
||
| def setup_test(): | ||
| """ | ||
| Setup test environment for migration with ethernet interface | ||
| """ | ||
| test.log.info("Setting up test environment") | ||
| remote_host_iface = utils_net.get_default_gateway( | ||
| iface_name=True, session=remote_session, force_dhcp=True, json=True) | ||
| params.update({"remote_host_iface": remote_host_iface}) | ||
|
|
||
| utils_net.create_linux_bridge_tmux(bridge_name, host_iface) | ||
| network_base.create_tap(tap_name, bridge_name, unpr_user, tap_mtu=tap_mtu) | ||
|
|
||
| utils_net.create_linux_bridge_tmux(bridge_name, remote_host_iface, session=remote_session) | ||
| network_base.create_tap(tap_name, bridge_name, unpr_user, | ||
| session=remote_session, tap_mtu=tap_mtu) | ||
|
|
||
| def run_test(): | ||
| """ | ||
| Run the main test: migration and verification | ||
| """ | ||
| test.log.info("TEST_STEP: Migrating VM to target host") | ||
| migration_obj.setup_connection() | ||
| virsh_ins.start(unpr_vm_obj.name, debug=True, ignore_status=False) | ||
| # test.log.debug("------------------------------------------") | ||
| # virsh_ins.dom_list(" --all", debug=True) | ||
| # time.sleep(420) | ||
|
|
||
| migration_obj.run_migration() | ||
|
|
||
| if not cancel_migration: | ||
| test.log.info("TEST_STEP: Checking VM network connectivity on target host") | ||
| if unpr_vm_obj.serial_console is not None: | ||
| unpr_vm_obj.cleanup_serial_console() | ||
| unpr_vm_obj.create_serial_console() | ||
| vm_session_after_mig = unpr_vm_obj.wait_for_serial_login(timeout=240) | ||
| vm_session_after_mig.cmd("dhclient -r; dhclient", timeout=120) | ||
|
|
||
| test.log.info("TEST_STEP: Testing guest ping to outside") | ||
| ips = {'outside_ip': outside_ip} | ||
| network_base.ping_check(params, ips, vm_session_after_mig) | ||
|
|
||
| if migrate_vm_back: | ||
| test.log.info("TEST_STEP: Migrating VM back to source host") | ||
| migration_obj.run_migration_back() | ||
|
|
||
|
|
||
| def teardown_test(): | ||
| """ | ||
| Cleanup test environment | ||
| """ | ||
| test.log.info("Cleaning up test environment") | ||
| network_base.delete_tap(tap_name) | ||
| network_base.delete_tap(tap_name, session=remote_session) | ||
|
|
||
| utils_net.delete_linux_bridge_tmux(bridge_name, iface_name=host_iface) | ||
| utils_net.delete_linux_bridge_tmux( | ||
| bridge_name, iface_name=params.get("remote_host_iface"), | ||
| session=remote_session) | ||
|
|
||
| bk_xml.sync() | ||
| virsh.destroy(unpr_vm_obj.name, unprivileged_user=unpr_user, debug=True) | ||
| virsh.undefine(unpr_vm_obj.name, options='--nvram', | ||
| unprivileged_user=unpr_user, debug=True) | ||
| migration_obj.cleanup_connection() | ||
| if remote_session: | ||
| remote_session.close() | ||
|
|
||
| # Parse test parameters | ||
| server_ip = params["server_ip"] = params.get("migrate_dest_host") | ||
| server_user = params.get("server_user") | ||
| server_pwd = params.get("server_pwd") or params.get("migrate_dest_pwd") | ||
| outside_ip = params.get("outside_ip") | ||
| bridge_name = params.get("bridge_name", "br0") | ||
| tap_name = params.get("tap_name", "mytap0") | ||
| tap_mtu = params.get("tap_mtu") | ||
| cancel_migration = params.get_boolean("cancel_migration") | ||
| migrate_vm_back = params.get_boolean("migrate_vm_back", True) | ||
| iface_attrs = eval(params.get("iface_attrs", "{}")) | ||
| host_iface = params.get('host_iface') | ||
| host_iface = host_iface if host_iface else utils_net.get_default_gateway( | ||
| iface_name=True, force_dhcp=True, json=True) | ||
|
|
||
| vm_name = params.get('main_vm') | ||
| unpr_user = params.get('test_user', 'test') | ||
| unpr_passwd = params.get('test_passwd', 'test') | ||
| dest_uri = params.get("virsh_migrate_desturi") % unpr_user | ||
| params.update({"virsh_migrate_desturi": dest_uri}) | ||
| connect_uri = f'qemu+ssh://{unpr_user}@localhost/session' | ||
|
|
||
| vmxml = vm_xml.VMXML.new_from_inactive_dumpxml(vm_name) | ||
| bk_xml = vmxml.copy() | ||
|
|
||
| remote_session = aexpect.remote.remote_login( | ||
| "ssh", server_ip, "22", server_user, server_pwd, r'[$#%]') | ||
|
|
||
| # virsh_ins = virsh.Virsh(uri=connect_uri) | ||
| # params["connect_uri"] = connect_uri | ||
| # params.update({"customized_uri": connect_uri}) | ||
| # unpr_vm_obj = create_unpr_user_and_vm(unpr_user, unpr_passwd, vm_name, remote_session) | ||
| # unpr_vm_obj.connect_uri = connect_uri | ||
| # migration_obj = base_steps.MigrationBase(test, unpr_vm_obj, params) | ||
|
|
||
| try: | ||
| setup_test() | ||
| run_test() | ||
| finally: | ||
| teardown_test() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -149,20 +149,29 @@ def _ping(): | |
| raise exceptions.TestFail('.'.join(failures)) | ||
|
|
||
|
|
||
| def create_tap(tap_name, bridge_name, user, flag=''): | ||
| def create_tap(tap_name, bridge_name, user, flag='', session=None, tap_mtu=''): | ||
| """ | ||
| Create tap device | ||
|
|
||
| :param tap_name: name of tap device | ||
| :param bridge_name: bridge to connect to | ||
| :param user: user with access | ||
| :param flag: additional flags for tap creation | ||
| :param session: session object for remote execution, None for local | ||
| :param tap_mtu: tap mtu setting value | ||
| """ | ||
| # Create tap device with ip command | ||
| tap_cmd = f'ip tuntap add mode tap user {user} group {user} name ' \ | ||
| f'{tap_name} {flag};ip link set {tap_name} up;' \ | ||
| f'ip link set {tap_name} master {bridge_name}' | ||
| # Execute command as root | ||
| process.run(tap_cmd, shell=True, verbose=True) | ||
| if session: | ||
| session.cmd(tap_cmd, timeout=60) | ||
| else: | ||
| process.run(tap_cmd, shell=True, verbose=True) | ||
|
|
||
| if tap_mtu: | ||
| set_tap_mtu(tap_name, tap_mtu, session) | ||
|
|
||
|
Comment on lines
+152
to
175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not chain with ';' — it masks failures; add timeouts and sanitize inputs. Semicolon-chained tap_cmd can succeed even if earlier subcommands fail. Also, shell=True and unquoted vars risk injection. Use '&&', quote inputs, and add a local timeout for parity with remote. Apply: @@
- tap_cmd = f'ip tuntap add mode tap user {user} group {user} name ' \
- f'{tap_name} {flag};ip link set {tap_name} up;' \
- f'ip link set {tap_name} master {bridge_name}'
+ tap_cmd = ' && '.join([
+ f"ip tuntap add mode tap user {user} group {user} name {tap_name} {flag}".strip(),
+ f"ip link set {tap_name} up",
+ f"ip link set {tap_name} master {bridge_name}",
+ ])
@@
- if session:
- session.cmd(tap_cmd, timeout=60)
- else:
- process.run(tap_cmd, shell=True, verbose=True)
+ if session:
+ session.cmd(tap_cmd, timeout=60)
+ else:
+ process.run(tap_cmd, shell=True, verbose=True, timeout=60)Optionally, restrict allowed characters in tap_name/bridge_name/flag or use shlex.quote for stronger hardening. Based on static analysis hints. 🧰 Tools🪛 Ruff (0.14.1)171-171: Function call with (S604) |
||
|
|
||
| def create_macvtap(macvtap_name, iface, user): | ||
|
|
@@ -193,23 +202,33 @@ def create_macvtap(macvtap_name, iface, user): | |
| return mac_addr | ||
|
|
||
|
|
||
| def set_tap_mtu(tap, mtu_size): | ||
| def set_tap_mtu(tap, mtu_size, session=None): | ||
| """ | ||
| Set mtu for tap/macvtap device | ||
|
|
||
| :param tap: tap/macvtap device name | ||
| :param mtu_size: mtu size to set | ||
| :param session: session object for remote execution, None for local | ||
| """ | ||
| process.run(f'ip link set dev {tap} mtu {mtu_size}') | ||
| mtu_cmd = f'ip link set dev {tap} mtu {mtu_size}' | ||
| if session: | ||
| session.cmd(mtu_cmd, timeout=30) | ||
| else: | ||
| process.run(mtu_cmd) | ||
|
|
||
|
Comment on lines
+205
to
218
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Local process.run lacks shell=True/timeout; align with remote path. Without shell=True and a timeout, local MTU set can behave inconsistently and hang. - mtu_cmd = f'ip link set dev {tap} mtu {mtu_size}'
- if session:
- session.cmd(mtu_cmd, timeout=30)
- else:
- process.run(mtu_cmd)
+ mtu_cmd = f'ip link set dev {tap} mtu {mtu_size}'
+ if session:
+ session.cmd(mtu_cmd, timeout=30)
+ else:
+ process.run(mtu_cmd, shell=True, timeout=30, verbose=True)🤖 Prompt for AI Agents |
||
|
|
||
| def delete_tap(tap_name): | ||
| def delete_tap(tap_name, session=None): | ||
| """ | ||
| Delete tap/macvtap device with given name | ||
|
|
||
| :param tap_name: name of tap/macvtap device | ||
| :param session: session object for remote execution, None for local | ||
| """ | ||
| process.run(f'ip l del {tap_name}', shell=True, ignore_status=True) | ||
| del_cmd = f'ip l del {tap_name}' | ||
| if session: | ||
| session.cmd(del_cmd) | ||
| else: | ||
| process.run(del_cmd, shell=True, ignore_status=True) | ||
|
|
||
|
|
||
| def prepare_vmxml_for_unprivileged_user(up_user, root_vmxml): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Ensure unprivileged user exists on the destination host before remote TAP ops.
create_tap on dest uses “user {unpr_user}”; if the user doesn’t exist remotely, tap creation fails.
If missing, add a guarded creation step before create_tap:
🏁 Script executed:
Length of output: 41
I need to examine the codebase to verify whether the concern about the unprivileged user is valid.
Length of output: 1853
🏁 Script executed:
Length of output: 1434
🏁 Script executed:
Length of output: 849
🏁 Script executed:
Length of output: 1041
I need to examine the
create_unpr_user_and_vmfunction and check whether it creates the user on both source and destination hosts.Length of output: 1921
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 1427
🏁 Script executed:
Length of output: 1308
I need to verify what the
create_tapfunction does with the user parameter on remote hosts.Length of output: 1115
🏁 Script executed:
Length of output: 1142
Create unprivileged user on destination host before remote TAP creation.
The
create_tapfunction executesip tuntap add mode tap user {user}which requires the user to exist on the target system. Currently, the unprivileged user is created locally (line 177 increate_unpr_user_and_vm), but the remotecreate_tapcall at lines 103-104 will fail because the user doesn't exist on the destination host.Add user creation on the remote host before line 103:
🤖 Prompt for AI Agents