Skip to content

Commit 62d4db0

Browse files
pb8ozulinx86
authored andcommitted
test: clean up network interfaces and SSH helper
Clean up the entwined network interfaces and SSH configuration. Now those two things should be decoupled and more clear to understand. Signed-off-by: Pablo Barbáchano <pablob@amazon.com>
1 parent cad6e41 commit 62d4db0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+528
-859
lines changed

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ def build(self, kernel=None, rootfs=None, **kwargs):
385385
# TBD only iff ext4 / rw
386386
shutil.copyfile(rootfs_path, rootfs_path2)
387387
vm.rootfs_file = rootfs_path2
388-
vm.ssh_config["ssh_key_path"] = rootfs.ssh_key().local_path()
388+
vm.ssh_key = rootfs.ssh_key().local_path()
389389
return vm
390390

391391
def kill(self):

tests/framework/artifacts.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -444,11 +444,6 @@ def net_ifaces(self):
444444
DEFAULT_NETMASK = 30
445445

446446

447-
def create_net_devices_configuration(num):
448-
"""Define configuration for the requested number of net devices."""
449-
return [NetIfaceConfig.with_id(i) for i in range(num)]
450-
451-
452447
@dataclass(frozen=True, repr=True)
453448
class NetIfaceConfig:
454449
"""Defines a network interface configuration."""

tests/framework/builder.py

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ def build(
124124

125125
# Download ssh key into the microvm root.
126126
ssh_key.download(vm.path)
127-
vm.ssh_config["ssh_key_path"] = ssh_key.local_path()
128-
os.chmod(vm.ssh_config["ssh_key_path"], 0o400)
127+
vm.ssh_key = ssh_key.local_path()
128+
os.chmod(vm.ssh_key, 0o400)
129129

130130
# Provide a default network configuration.
131131
if net_ifaces is None or len(net_ifaces) == 0:
@@ -135,18 +135,7 @@ def build(
135135

136136
# Configure network interfaces using artifacts.
137137
for iface in ifaces:
138-
vm.create_tap_and_ssh_config(
139-
host_ip=iface.host_ip,
140-
guest_ip=iface.guest_ip,
141-
netmask_len=iface.netmask,
142-
tapname=iface.tap_name,
143-
)
144-
response = vm.network.put(
145-
iface_id=iface.dev_name,
146-
host_dev_name=iface.tap_name,
147-
guest_mac=iface.guest_mac,
148-
)
149-
assert vm.api_session.is_status_no_content(response.status_code)
138+
vm.add_net_iface(iface)
150139

151140
with open(config.local_path(), encoding="utf-8") as microvm_config_file:
152141
microvm_config = json.load(microvm_config_file)
@@ -244,16 +233,11 @@ def build_from_snapshot(
244233
else vm.create_jailed_resource(disk)
245234
)
246235

247-
vm.ssh_config["ssh_key_path"] = snapshot.ssh_key.local_path()
236+
vm.ssh_key = snapshot.ssh_key.local_path()
248237

249238
# Create network interfaces.
250239
for iface in snapshot.net_ifaces:
251-
vm.create_tap_and_ssh_config(
252-
host_ip=iface.host_ip,
253-
guest_ip=iface.guest_ip,
254-
netmask_len=iface.netmask,
255-
tapname=iface.tap_name,
256-
)
240+
vm.add_net_iface(iface, api=False)
257241

258242
full_fc_version = vm.version.get_from_api().json()["firecracker_version"]
259243
if utils.compare_dirty_versions(full_fc_version, "1.0.0") > 0:
@@ -278,12 +262,6 @@ def build_from_snapshot(
278262
)
279263
status_ok = vm.api_session.is_status_no_content(response.status_code)
280264

281-
# Verify response status and cleanup if needed before assert.
282-
if not status_ok:
283-
# Destroy VM here before we assert.
284-
vm.kill()
285-
del vm
286-
287265
assert status_ok, response.text
288266

289267
# Return a resumed microvm.

tests/framework/microvm.py

Lines changed: 33 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import uuid
1919
import weakref
2020
from collections import namedtuple
21-
from functools import cached_property
21+
from functools import lru_cache
2222
from pathlib import Path
2323
from threading import Lock
2424
from typing import Optional
@@ -30,6 +30,7 @@
3030
import host_tools.memory as mem_tools
3131
import host_tools.network as net_tools
3232
from framework import utils
33+
from framework.artifacts import NetIfaceConfig
3334
from framework.defs import FC_PID_FILE_NAME, MAX_API_CALL_DURATION_MS
3435
from framework.http import Session
3536
from framework.jailer import JailerContext
@@ -157,15 +158,6 @@ def __init__(
157158
# Initalize memory monitor
158159
self.memory_monitor = None
159160

160-
# The ssh config dictionary is populated with information about how
161-
# to connect to a microVM that has ssh capability. The path of the
162-
# private key is populated by microvms with ssh capabilities and the
163-
# hostname is set from the MAC address used to configure the microVM.
164-
self._ssh_config = {
165-
"username": "root",
166-
"netns_file_path": self.jailer.netns_file_path(),
167-
}
168-
169161
# iface dictionary
170162
self.iface = {}
171163

@@ -319,11 +311,6 @@ def log_data(self):
319311
log_data = self.__log_data
320312
return log_data
321313

322-
@property
323-
def ssh_config(self):
324-
"""Get the ssh configuration used to ssh into some microVMs."""
325-
return self._ssh_config
326-
327314
@property
328315
def state(self):
329316
"""Get the InstanceInfo property and return the state field."""
@@ -714,82 +701,31 @@ def patch_drive(self, drive_id, file):
714701
)
715702
assert self.api_session.is_status_no_content(response.status_code)
716703

717-
def ssh_network_config(
718-
self,
719-
network_config,
720-
iface_id,
721-
tx_rate_limiter=None,
722-
rx_rate_limiter=None,
723-
tapname=None,
724-
):
725-
"""Create a host tap device and a guest network interface.
726-
727-
'network_config' is used to generate 2 IPs: one for the tap device
728-
and one for the microvm. Adds the hostname of the microvm to the
729-
ssh_config dictionary.
730-
:param network_config: UniqueIPv4Generator instance
731-
:param iface_id: the interface id for the API request
732-
the guest on this interface towards the MMDS address are
733-
intercepted and processed by the device model.
734-
:param tx_rate_limiter: limit the tx rate
735-
:param rx_rate_limiter: limit the rx rate
736-
:return: an instance of the tap which needs to be kept around until
737-
cleanup is desired, the configured guest and host ips, respectively.
738-
"""
739-
# Create tap before configuring interface.
740-
tapname = tapname or (self.id[:8] + "tap" + iface_id)
741-
# The guest is hardcoded to expect an IP in a /30 network,
742-
# so we request the IPs to be aligned on a /30 network
743-
# See https://github.com/firecracker-microvm/firecracker/blob/main/resources/tests/fcnet-setup.sh#L21
744-
# file:../../resources/tests/fcnet-setup.sh#L21
745-
netmask_len = 30
746-
(host_ip, guest_ip) = network_config.get_next_available_ips(2, netmask_len)
747-
tap = self.create_tap_and_ssh_config(host_ip, guest_ip, netmask_len, tapname)
748-
guest_mac = net_tools.mac_from_ip(guest_ip)
749-
750-
response = self.network.put(
751-
iface_id=iface_id,
752-
host_dev_name=tapname,
753-
guest_mac=guest_mac,
754-
tx_rate_limiter=tx_rate_limiter,
755-
rx_rate_limiter=rx_rate_limiter,
756-
)
757-
assert self._api_session.is_status_no_content(response.status_code)
758-
759-
return tap, host_ip, guest_ip
760-
761-
def create_tap_and_ssh_config(self, host_ip, guest_ip, netmask_len, tapname=None):
762-
"""Create tap device and configure ssh."""
763-
assert tapname is not None
764-
tap = net_tools.Tap(
765-
tapname, self.jailer.netns, ip="{}/{}".format(host_ip, netmask_len)
766-
)
767-
self.config_ssh(guest_ip)
768-
return tap
769-
770-
def config_ssh(self, guest_ip):
771-
"""Configure ssh."""
772-
self.ssh_config["hostname"] = guest_ip
773-
774-
def add_net_iface(self, iface, tx_rate_limiter=None, rx_rate_limiter=None):
704+
def add_net_iface(self, iface=None, api=True, **kwargs):
775705
"""Add a network interface"""
706+
if iface is None:
707+
iface = NetIfaceConfig.with_id(len(self.iface))
776708
tap = net_tools.Tap(
777709
iface.tap_name, self.jailer.netns, ip=f"{iface.host_ip}/{iface.netmask}"
778710
)
779-
self.config_ssh(iface.guest_ip)
780-
response = self.network.put(
781-
iface_id=iface.dev_name,
782-
host_dev_name=iface.tap_name,
783-
guest_mac=iface.guest_mac,
784-
tx_rate_limiter=tx_rate_limiter,
785-
rx_rate_limiter=rx_rate_limiter,
786-
)
787-
assert self.api_session.is_status_no_content(response.status_code)
788711
self.iface[iface.dev_name] = {
789712
"iface": iface,
790713
"tap": tap,
791714
}
792715

716+
# If api, call it... there may be cases when we don't want it, for
717+
# example during restore
718+
if api:
719+
response = self.network.put(
720+
iface_id=iface.dev_name,
721+
host_dev_name=iface.tap_name,
722+
guest_mac=iface.guest_mac,
723+
**kwargs,
724+
)
725+
assert self.api_session.is_status_no_content(response.status_code)
726+
727+
return iface
728+
793729
def start(self, check=True):
794730
"""Start the microvm.
795731
@@ -868,10 +804,22 @@ def restore_from_snapshot(
868804
assert response.ok, response.content
869805
return True
870806

871-
@cached_property
807+
@lru_cache
808+
def ssh_iface(self, iface_idx=0):
809+
"""Return a cached SSH connection on a given interface id."""
810+
guest_ip = list(self.iface.values())[iface_idx]["iface"].guest_ip
811+
self.ssh_key = Path(self.ssh_key)
812+
return net_tools.SSHConnection(
813+
netns_path=self.jailer.netns_file_path(),
814+
ssh_key=self.ssh_key,
815+
user="root",
816+
host=guest_ip,
817+
)
818+
819+
@property
872820
def ssh(self):
873-
"""Return a cached SSH connection"""
874-
return net_tools.SSHConnection(self.ssh_config)
821+
"""Return a cached SSH connection on the 1st interface"""
822+
return self.ssh_iface(0)
875823

876824
def start_console_logger(self, log_fifo):
877825
"""

tests/framework/s3fetcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def init_vm_resources(self, microvm_image_name, microvm):
142142
if resource_key.endswith(self.MICROVM_IMAGE_SSH_KEY_SUFFIX):
143143
# Add the key path to the config dictionary and set
144144
# permissions.
145-
microvm.ssh_config["ssh_key_path"] = microvm_dest_path
145+
microvm.ssh_key = microvm_dest_path
146146
os.chmod(microvm_dest_path, 0o400)
147147

148148
def list_microvm_images(self, capability_filter: List[str] = None):

tests/framework/stats/producer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ def produce(self) -> Any:
3030
"""Return the output of the executed ssh command."""
3131
rc, stdout, stderr = self._ssh_connection.execute_command(self._cmd)
3232
assert rc == 0
33-
assert stderr.read() == ""
34-
35-
return stdout.read()
33+
assert stderr == ""
34+
return stdout
3635

3736

3837
class HostCommand(Producer):

tests/framework/utils.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,10 @@ def get_free_mem_ssh(ssh_connection):
396396
_, stdout, stderr = ssh_connection.execute_command(
397397
"cat /proc/meminfo | grep MemAvailable"
398398
)
399-
assert stderr.read() == ""
399+
assert stderr == ""
400400

401401
# Split "MemAvailable: 123456 kB" and validate it
402-
meminfo_data = stdout.read().split()
402+
meminfo_data = stdout.split()
403403
if len(meminfo_data) == 3:
404404
# Return the middle element in the array
405405
return int(meminfo_data[1])
@@ -540,8 +540,8 @@ def avg(thread_name):
540540
def run_guest_cmd(ssh_connection, cmd, expected, use_json=False):
541541
"""Runs a shell command at the remote accessible via SSH"""
542542
_, stdout, stderr = ssh_connection.execute_command(cmd)
543-
assert stderr.read() == ""
544-
stdout = stdout.read() if not use_json else json.loads(stdout.read())
543+
assert stderr == ""
544+
stdout = stdout if not use_json else json.loads(stdout)
545545
assert stdout == expected
546546

547547

@@ -664,7 +664,7 @@ def generate_mmds_session_token(ssh_connection, ipv4_address, token_ttl):
664664
cmd += ' -H "X-metadata-token-ttl-seconds: {}"'.format(token_ttl)
665665
cmd += " http://{}/latest/api/token".format(ipv4_address)
666666
_, stdout, _ = ssh_connection.execute_command(cmd)
667-
token = stdout.read()
667+
token = stdout
668668

669669
return token
670670

@@ -776,21 +776,21 @@ def guest_run_fio_iteration(ssh_connection, iteration):
776776
iteration
777777
)
778778
exit_code, _, stderr = ssh_connection.execute_command(fio)
779-
assert exit_code == 0, stderr.read()
779+
assert exit_code == 0, stderr
780780

781781

782782
def check_filesystem(ssh_connection, disk_fmt, disk):
783783
"""Check for filesystem corruption inside a microVM."""
784784
cmd = "fsck.{} -n {}".format(disk_fmt, disk)
785785
exit_code, _, stderr = ssh_connection.execute_command(cmd)
786-
assert exit_code == 0, stderr.read()
786+
assert exit_code == 0, stderr
787787

788788

789789
def check_entropy(ssh_connection):
790790
"""Check that we can get random numbers from /dev/hwrng"""
791791
cmd = "dd if=/dev/hwrng of=/dev/null bs=4096 count=1"
792792
exit_code, _, stderr = ssh_connection.execute_command(cmd)
793-
assert exit_code == 0, stderr.read()
793+
assert exit_code == 0, stderr
794794

795795

796796
@retry(delay=0.5, tries=5)

tests/framework/utils_cpuid.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,8 @@ def check_guest_cpuid_output(
8585
"""Parse cpuid output inside guest and match with expected one."""
8686
_, stdout, stderr = vm.ssh.execute_command(guest_cmd)
8787

88-
assert stderr.read() == ""
89-
while True:
90-
line = stdout.readline()
88+
assert stderr == ""
89+
for line in stdout.split("\n"):
9190
if line != "":
9291
# All the keys have been matched. Stop.
9392
if not expected_key_value_store:
@@ -134,7 +133,7 @@ def build_cpuid_dict(raw_cpuid_output):
134133
"""Build CPUID dict based on raw cpuid output"""
135134
cpuid_dict = {}
136135
ptrn = re.compile("^ *(.*) (.*): eax=(.*) ebx=(.*) ecx=(.*) edx=(.*)$")
137-
for line in raw_cpuid_output:
136+
for line in raw_cpuid_output.strip().split("\n"):
138137
match = re.match(ptrn, line)
139138
assert match, f"`{line}` does not match the regex pattern."
140139
leaf, subleaf, eax, ebx, ecx, edx = [int(x, 16) for x in match.groups()]
@@ -159,7 +158,7 @@ def get_guest_cpuid(vm, leaf=None, subleaf=None):
159158
else:
160159
read_cpuid_cmd = "cpuid -r | sed '/CPU 1/q' | grep -v CPU"
161160
_, stdout, stderr = vm.ssh.execute_command(read_cpuid_cmd)
162-
assert stderr.read() == ""
161+
assert stderr == ""
163162

164163
return build_cpuid_dict(stdout)
165164

tests/framework/utils_iperf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ def spawn_iperf3_client(self, client_idx):
111111
)
112112
rc, stdout, stderr = self._microvm.ssh.execute_command(pinned_cmd)
113113

114-
assert rc == 0, stderr.read()
114+
assert rc == 0, stderr
115115

116-
return stdout.read()
116+
return stdout
117117

118118
def guest_command(self, port_offset):
119119
"""Builds the command used for spawning an iperf3 client in the guest"""

0 commit comments

Comments
 (0)