Skip to content

Commit 302851d

Browse files
committed
tests: MMDS works when configuring VM from file
Ensure MMDS works as expected (both default and V2) when spawning microVM from a configuration file. Signed-off-by: Luminita Voicu <[email protected]>
1 parent 3e217c1 commit 302851d

File tree

6 files changed

+302
-39
lines changed

6 files changed

+302
-39
lines changed

tests/framework/jailer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ def setup(self, use_ramdisk=False):
247247
)
248248
utils.run_cmd(cmd)
249249

250-
if self.netns:
250+
if self.netns and self.netns not in utils.run_cmd('ip netns list')[1]:
251251
utils.run_cmd('ip netns add {}'.format(self.netns))
252252

253253
def cleanup(self):

tests/framework/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,3 +631,27 @@ def is_io_uring_supported():
631631
return compare_versions(
632632
get_kernel_version(), MIN_KERNEL_VERSION_FOR_IO_URING
633633
) >= 0
634+
635+
636+
def generate_mmds_session_token(ssh_connection, ipv4_address, token_ttl):
637+
"""Generate session token used for MMDS V2 requests."""
638+
cmd = 'curl -m 2 -s'
639+
cmd += ' -X PUT'
640+
cmd += ' -H "X-metadata-token-ttl-seconds: {}"'.format(token_ttl)
641+
cmd += ' http://{}/latest/api/token'.format(ipv4_address)
642+
_, stdout, _ = ssh_connection.execute_command(cmd)
643+
token = stdout.read()
644+
645+
return token
646+
647+
648+
def generate_mmds_v2_get_request(ipv4_address, token, app_json=True):
649+
"""Build `GET` request to fetch metadata from MMDS when using V2."""
650+
cmd = 'curl -m 2 -s'
651+
cmd += ' -X GET'
652+
cmd += ' -H "X-metadata-token: {}"'.format(token)
653+
if app_json:
654+
cmd += ' -H "Accept: application/json"'
655+
cmd += ' http://{}/'.format(ipv4_address)
656+
657+
return cmd
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"boot-source": {
3+
"kernel_image_path": "vmlinux.bin",
4+
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off",
5+
"initrd_path": null
6+
},
7+
"drives": [
8+
{
9+
"drive_id": "rootfs",
10+
"path_on_host": "bionic.rootfs.ext4",
11+
"is_root_device": true,
12+
"partuuid": null,
13+
"is_read_only": false,
14+
"cache_type": "Unsafe",
15+
"io_engine": "Sync",
16+
"rate_limiter": null
17+
}
18+
],
19+
"machine-config": {
20+
"vcpu_count": 2,
21+
"mem_size_mib": 1024,
22+
"track_dirty_pages": false
23+
},
24+
"balloon": null,
25+
"network-interfaces": [
26+
{
27+
"iface_id": "1",
28+
"host_dev_name": "tap0",
29+
"guest_mac": "06:00:c0:a8:00:02",
30+
"rx_rate_limiter": null,
31+
"tx_rate_limiter": null
32+
}
33+
],
34+
"vsock": null,
35+
"logger": null,
36+
"metrics": null,
37+
"mmds-config": {
38+
"network_interfaces": ["1"]
39+
}
40+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"boot-source": {
3+
"kernel_image_path": "vmlinux.bin",
4+
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off",
5+
"initrd_path": null
6+
},
7+
"drives": [
8+
{
9+
"drive_id": "rootfs",
10+
"path_on_host": "bionic.rootfs.ext4",
11+
"is_root_device": true,
12+
"partuuid": null,
13+
"is_read_only": false,
14+
"cache_type": "Unsafe",
15+
"io_engine": "Sync",
16+
"rate_limiter": null
17+
}
18+
],
19+
"machine-config": {
20+
"vcpu_count": 2,
21+
"mem_size_mib": 1024,
22+
"smt": false,
23+
"track_dirty_pages": false
24+
},
25+
"balloon": null,
26+
"network-interfaces": [
27+
{
28+
"iface_id": "1",
29+
"host_dev_name": "tap0",
30+
"guest_mac": "06:00:c0:a8:00:02",
31+
"rx_rate_limiter": null,
32+
"tx_rate_limiter": null
33+
}
34+
],
35+
"vsock": null,
36+
"logger": null,
37+
"metrics": null,
38+
"mmds-config": {
39+
"network_interfaces": ["1"],
40+
"ipv4_address": "169.254.169.250",
41+
"version": "V2"
42+
}
43+
}

tests/integration_tests/functional/test_cmd_line_start.py

Lines changed: 182 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@
1212
import pytest
1313

1414
from framework import utils
15+
from framework.artifacts import NetIfaceConfig
16+
from framework.utils import generate_mmds_session_token, \
17+
generate_mmds_v2_get_request
18+
import host_tools.network as net_tools
1519

1620

1721
def _configure_vm_from_json(test_microvm, vm_config_file):
18-
"""Configure a microvm using a file sent as command line parameter.
22+
"""
23+
Configure a microvm using a file sent as command line parameter.
1924
2025
Create resources needed for the configuration of the microvm and
2126
set as configuration file a copy of the file that was passed as
@@ -42,7 +47,8 @@ def _configure_vm_from_json(test_microvm, vm_config_file):
4247

4348

4449
def _add_metadata_file(test_microvm, metadata_file):
45-
"""Configure the microvm using a metadata file.
50+
"""
51+
Configure the microvm using a metadata file.
4652
4753
Given a test metadata file this creates a copy of the file and
4854
uses the copy to configure the microvm.
@@ -55,6 +61,70 @@ def _add_metadata_file(test_microvm, metadata_file):
5561
test_microvm.metadata_file = vm_metadata_path
5662

5763

64+
def _configure_network_interface(test_microvm):
65+
"""
66+
Create tap interface before spawning the microVM.
67+
68+
The network namespace and tap interface have to be created
69+
beforehand when starting the microVM from a config file.
70+
"""
71+
# Create network namespace.
72+
utils.run_cmd(f"ip netns add {test_microvm.jailer.netns}")
73+
74+
# Create tap device and SSH config.
75+
net_iface = NetIfaceConfig()
76+
_tap = test_microvm.create_tap_and_ssh_config(net_iface.host_ip,
77+
net_iface.guest_ip,
78+
net_iface.netmask,
79+
net_iface.tap_name)
80+
81+
82+
def _build_cmd_to_fetch_metadata(ssh_connection, version, ipv4_address):
83+
"""
84+
Build command to fetch metadata from the guest's side.
85+
86+
The request is built based on the MMDS version configured.
87+
If MMDSv2 is used, a session token must be created before
88+
the `GET` request.
89+
"""
90+
# Fetch data from MMDS from the guest's side.
91+
if version == "V1":
92+
cmd = 'curl -s -H "Accept: application/json" '
93+
cmd += 'http://{}'.format(ipv4_address)
94+
95+
else:
96+
# If MMDS is configured to version 2, so we need to create
97+
# the session token first.
98+
token = generate_mmds_session_token(
99+
ssh_connection,
100+
ipv4_address,
101+
token_ttl=60
102+
)
103+
cmd = generate_mmds_v2_get_request(ipv4_address, token)
104+
105+
return cmd
106+
107+
108+
def _get_optional_fields_from_file(vm_config_file):
109+
"""
110+
Retrieve optional `version` and `ipv4_address` fields from MMDS config.
111+
112+
Parse the vm config json file and retrieves optional fields from MMDS
113+
config. Default values are used for the fields that are not specified.
114+
115+
:return: a pair of (version, ipv4_address) fields from mmds config.
116+
"""
117+
# Get MMDS version and IPv4 address configured from the file.
118+
with open(vm_config_file, encoding='utf-8') as json_file:
119+
mmds_config = json.load(json_file)["mmds-config"]
120+
# Default to V1 if version is not specified.
121+
version = mmds_config.get("version", "V1")
122+
# Set to default if IPv4 is not specified .
123+
ipv4_address = mmds_config.get("ipv4_address", "169.254.169.254")
124+
125+
return version, ipv4_address
126+
127+
58128
@pytest.mark.parametrize(
59129
"vm_config_file",
60130
["framework/vm_config.json"]
@@ -287,19 +357,125 @@ def test_start_with_invalid_metadata(test_microvm_with_api):
287357
)
288358

289359

290-
def test_with_config_and_metadata_no_api(test_microvm_with_api):
360+
@pytest.mark.parametrize(
361+
"vm_config_file",
362+
[
363+
"framework/vm_config_with_mmdsv1.json",
364+
"framework/vm_config_with_mmdsv2.json"
365+
]
366+
)
367+
def test_config_start_and_mmds_with_api(test_microvm_with_api, vm_config_file):
291368
"""
292-
Test microvm start when config/mmds and API server thread is disable.
369+
Test MMDS behavior when the microvm is configured from file.
293370
294371
@type: functional
295372
"""
296-
vm_config_file = "framework/vm_config.json"
297-
metadata_file = "../resources/tests/metadata.json"
373+
test_microvm = test_microvm_with_api
374+
375+
_configure_vm_from_json(test_microvm, vm_config_file)
376+
_configure_network_interface(test_microvm)
377+
378+
# Network namespace has already been created.
379+
test_microvm.spawn()
380+
381+
response = test_microvm.machine_cfg.get()
382+
assert test_microvm.api_session.is_status_ok(response.status_code)
383+
assert test_microvm.state == "Running"
384+
385+
data_store = {
386+
'latest': {
387+
'meta-data': {
388+
'ami-id': 'ami-12345678',
389+
'reservation-id': 'r-fea54097'
390+
}
391+
}
392+
}
393+
394+
# MMDS should be empty by default.
395+
response = test_microvm.mmds.get()
396+
assert test_microvm.api_session.is_status_ok(response.status_code)
397+
assert response.json() == {}
398+
399+
# Populate MMDS with data.
400+
response = test_microvm.mmds.put(json=data_store)
401+
assert test_microvm.api_session.is_status_no_content(response.status_code)
402+
403+
# Ensure the MMDS contents have been successfully updated.
404+
response = test_microvm.mmds.get()
405+
assert test_microvm.api_session.is_status_ok(response.status_code)
406+
assert response.json() == data_store
407+
408+
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
409+
410+
# Get MMDS version and IPv4 address configured from the file.
411+
version, ipv4_address = _get_optional_fields_from_file(vm_config_file)
412+
413+
cmd = 'ip route add {} dev eth0'.format(ipv4_address)
414+
_, stdout, stderr = ssh_connection.execute_command(cmd)
415+
assert stderr.read() == stdout.read() == ''
416+
417+
# Fetch data from MMDS from the guest's side.
418+
cmd = _build_cmd_to_fetch_metadata(ssh_connection, version, ipv4_address)
419+
cmd += '/latest/meta-data/'
420+
_, stdout, _ = ssh_connection.execute_command(cmd)
421+
assert json.load(stdout) == data_store['latest']['meta-data']
422+
423+
# Validate MMDS configuration.
424+
response = test_microvm.full_cfg.get()
425+
assert test_microvm.api_session.is_status_ok(response.status_code)
426+
assert response.json()["mmds-config"] == {
427+
'network_interfaces': ['1'],
428+
'ipv4_address': ipv4_address,
429+
'version': version
430+
}
298431

432+
433+
@pytest.mark.parametrize(
434+
"vm_config_file",
435+
[
436+
"framework/vm_config_with_mmdsv1.json",
437+
"framework/vm_config_with_mmdsv2.json"
438+
]
439+
)
440+
@pytest.mark.parametrize(
441+
"metadata_file",
442+
["../resources/tests/metadata.json"]
443+
)
444+
def test_with_config_and_metadata_no_api(
445+
test_microvm_with_api,
446+
vm_config_file,
447+
metadata_file
448+
):
449+
"""
450+
Test microvm start when config/mmds and API server thread is disabled.
451+
452+
Ensures the metadata is stored successfully inside the MMDS and
453+
is available to reach from the guest's side.
454+
455+
@type: functional
456+
"""
299457
test_microvm = test_microvm_with_api
300458

301459
_configure_vm_from_json(test_microvm, vm_config_file)
302460
_add_metadata_file(test_microvm, metadata_file)
461+
_configure_network_interface(test_microvm)
303462
test_microvm.jailer.extra_args.update({'no-api': None})
304463

305464
test_microvm.spawn()
465+
466+
ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)
467+
468+
# Get MMDS version and IPv4 address configured from the file.
469+
version, ipv4_address = _get_optional_fields_from_file(vm_config_file)
470+
471+
cmd = 'ip route add {} dev eth0'.format(ipv4_address)
472+
_, stdout, stderr = ssh_connection.execute_command(cmd)
473+
assert stderr.read() == stdout.read() == ''
474+
475+
# Fetch data from MMDS from the guest's side.
476+
cmd = _build_cmd_to_fetch_metadata(ssh_connection, version, ipv4_address)
477+
_, stdout, _ = ssh_connection.execute_command(cmd)
478+
479+
# Compare response against the expected MMDS contents.
480+
with open(metadata_file, encoding='utf-8') as metadata:
481+
assert json.load(stdout) == json.load(metadata)

0 commit comments

Comments
 (0)