Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/host_tools/network.py
1956 views
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Utilities for test host microVM network setup."""
4
5
import os
6
import socket
7
import struct
8
from io import StringIO
9
from nsenter import Namespace
10
from retry import retry
11
12
import framework.mpsing as mpsing
13
import framework.utils as utils
14
15
16
class SSHConnection:
17
"""SSHConnection encapsulates functionality for microVM SSH interaction.
18
19
This class should be instantiated as part of the ssh fixture with the
20
the hostname obtained from the MAC address, the username for logging into
21
the image and the path of the ssh key.
22
23
The ssh config dictionary contains the following fields:
24
* hostname
25
* username
26
* ssh_key_path
27
28
This translates into an SSH connection as follows:
29
ssh -i ssh_key_path username@hostname
30
"""
31
32
def __init__(self, ssh_config):
33
"""Instantiate a SSH client and connect to a microVM."""
34
self.netns_file_path = ssh_config['netns_file_path']
35
self.ssh_config = ssh_config
36
assert os.path.exists(ssh_config['ssh_key_path'])
37
38
self._init_connection()
39
40
def execute_command(self, cmd_string):
41
"""Execute the command passed as a string in the ssh context."""
42
exit_code, stdout, stderr = self._exec(cmd_string)
43
return exit_code, StringIO(stdout), StringIO(stderr)
44
45
def scp_file(self, local_path, remote_path):
46
"""Copy a files to the VM using scp."""
47
cmd = ('scp -o StrictHostKeyChecking=no'
48
' -o UserKnownHostsFile=/dev/null'
49
' -i {} {} {}@{}:{}').format(
50
self.ssh_config['ssh_key_path'],
51
local_path,
52
self.ssh_config['username'],
53
self.ssh_config['hostname'],
54
remote_path
55
)
56
if self.netns_file_path:
57
with Namespace(self.netns_file_path, 'net'):
58
utils.run_cmd(cmd)
59
else:
60
utils.run_cmd(cmd)
61
62
def scp_get_file(self, remote_path, local_path):
63
"""Copy files from the VM using scp."""
64
cmd = ('scp -o StrictHostKeyChecking=no'
65
' -o UserKnownHostsFile=/dev/null'
66
' -i {} {}@{}:{} {}').format(
67
self.ssh_config['ssh_key_path'],
68
self.ssh_config['username'],
69
self.ssh_config['hostname'],
70
remote_path,
71
local_path
72
)
73
if self.netns_file_path:
74
with Namespace(self.netns_file_path, 'net'):
75
utils.run_cmd(cmd)
76
else:
77
utils.run_cmd(cmd)
78
79
@retry(ConnectionError, delay=0.1, tries=20)
80
def _init_connection(self):
81
"""Create an initial SSH client connection (retry until it works).
82
83
Since we're connecting to a microVM we just started, we'll probably
84
have to wait for it to boot up and start the SSH server.
85
We'll keep trying to execute a remote command that can't fail
86
(`/bin/true`), until we get a successful (0) exit code.
87
"""
88
ecode, _, _ = self._exec("true")
89
if ecode != 0:
90
raise ConnectionError
91
92
def _exec(self, cmd):
93
"""Private function that handles the ssh client invocation."""
94
def _exec_raw(_cmd):
95
# pylint: disable=subprocess-run-check
96
cp = utils.run_cmd([
97
"ssh",
98
"-q",
99
"-o", "ConnectTimeout=1",
100
"-o", "StrictHostKeyChecking=no",
101
"-o", "UserKnownHostsFile=/dev/null",
102
"-i", self.ssh_config["ssh_key_path"],
103
"{}@{}".format(
104
self.ssh_config["username"],
105
self.ssh_config["hostname"]
106
),
107
_cmd],
108
ignore_return_code=True)
109
110
_res = (
111
cp.returncode,
112
cp.stdout,
113
cp.stderr
114
)
115
return _res
116
117
# TODO: If a microvm runs in a particular network namespace, we have to
118
# temporarily switch to that namespace when doing something that routes
119
# packets over the network, otherwise the destination will not be
120
# reachable. Use a better setup/solution at some point!
121
if self.netns_file_path:
122
with Namespace(self.netns_file_path, 'net'):
123
res = _exec_raw(cmd)
124
else:
125
res = _exec_raw(cmd)
126
return res
127
128
129
class NoMoreIPsError(Exception):
130
"""No implementation required."""
131
132
133
class InvalidIPCount(Exception):
134
"""No implementation required."""
135
136
137
class UniqueIPv4Generator(mpsing.MultiprocessSingleton):
138
"""Singleton implementation.
139
140
Each microVM needs to have a unique IP on the host network.
141
142
This class should be instantiated once per test session. All the
143
microvms will have to use the same netmask length for
144
the generator to work.
145
146
This class will only generate IP addresses from the ranges
147
192.168.0.0 - 192.168.255.255 and 172.16.0.0 - 172.31.255.255 which
148
are the private IPs sub-networks.
149
150
For a network mask of 30 bits, the UniqueIPv4Generator can generate up
151
to 16320 sub-networks, each with 2 valid IPs from the
152
192.168.0.0 - 192.168.255.255 range and 244800 sub-networks from the
153
172.16.0.0 - 172.31.255.255 range.
154
"""
155
156
@staticmethod
157
def __ip_to_int(ip: str):
158
return int.from_bytes(socket.inet_aton(ip), 'big')
159
160
def __init__(self):
161
"""Don't call directly. Use cls.instance() instead."""
162
super().__init__()
163
164
# For the IPv4 address range 192.168.0.0 - 192.168.255.255, the mask
165
# length is 16 bits. This means that the netmask_len used to
166
# initialize the class can't be smaller that 16. For now we stick to
167
# the default mask length = 30.
168
self.netmask_len = 30
169
self.ip_range = [
170
('192.168.0.0', '192.168.255.255'),
171
('172.16.0.0', '172.31.255.255')
172
]
173
# We start by consuming IPs from the first defined range.
174
self.ip_range_index = 0
175
# The ip_range_min_index is the first IP in the range that can be used.
176
# For the first range, this corresponds to "192.168.0.0".
177
self.ip_range_min_index = 0
178
179
# The ip_range_max_index is the last IP in the range that can be used.
180
# For the first range, this corresponds to "192.168.255.255".
181
self.ip_range_max_index = 1
182
self.next_valid_subnet_id = self.__ip_to_int(
183
self.ip_range[self.ip_range_index][0]
184
)
185
186
# The subnet_len contains the number of valid IPs in a subnet and it is
187
# used to increment the next_valid_subnet_id once a request for a
188
# subnet is issued.
189
self.subnet_max_ip_count = (1 << 32 - self.netmask_len)
190
191
def __ensure_next_subnet(self):
192
"""Raise an exception if there are no subnets available."""
193
max_ip_as_int = self.__ip_to_int(
194
self.ip_range[self.ip_range_index][self.ip_range_max_index]
195
)
196
197
# Check if there are any IPs left to use from the current range.
198
if (
199
self.next_valid_subnet_id + self.subnet_max_ip_count
200
> max_ip_as_int
201
):
202
# Check if there are any other IP ranges.
203
if self.ip_range_index < len(self.ip_range) - 1:
204
# Move to the next IP range.
205
self.ip_range_index += 1
206
self.next_valid_subnet_id = self.__ip_to_int(
207
self.ip_range[self.ip_range_index][self.ip_range_min_index]
208
)
209
else:
210
# There are no other ranges defined, so no more unassigned IPs.
211
raise NoMoreIPsError
212
213
def get_netmask_len(self):
214
"""Return the network mask length."""
215
return self.netmask_len
216
217
@mpsing.ipcmethod
218
def get_next_available_subnet_range(self):
219
"""Return a pair of IPS encompassing an unused subnet.
220
221
:return: range of IPs (defined as a pair) from an unused subnet.
222
The mask used is the one defined when instantiating the
223
UniqueIPv4Generator class.
224
"""
225
self.__ensure_next_subnet()
226
next_available_subnet = (
227
socket.inet_ntoa(
228
struct.pack('!L', self.next_valid_subnet_id)
229
),
230
socket.inet_ntoa(
231
struct.pack(
232
'!L',
233
self.next_valid_subnet_id +
234
(self.subnet_max_ip_count - 1)
235
)
236
)
237
)
238
239
self.next_valid_subnet_id += self.subnet_max_ip_count
240
return next_available_subnet
241
242
@mpsing.ipcmethod
243
def get_next_available_ips(self, count):
244
"""Return a count of unique IPs.
245
246
Raises InvalidIPCount when the requested IPs number is > than the
247
length of the subnet mask -2. Two IPs from the subnet are reserved
248
because the first address is the subnet identifier and the last IP is
249
the broadcast IP.
250
251
:param count: number of unique IPs to return
252
:return: list of IPs as a list of strings
253
"""
254
if count > self.subnet_max_ip_count - 2:
255
raise InvalidIPCount
256
257
self.__ensure_next_subnet()
258
# The first IP in a subnet is the subnet identifier.
259
next_available_ip = self.next_valid_subnet_id + 1
260
ip_list = []
261
for _ in range(count):
262
ip_as_string = socket.inet_ntoa(
263
struct.pack('!L', next_available_ip)
264
)
265
ip_list.append(ip_as_string)
266
next_available_ip += 1
267
self.next_valid_subnet_id += self.subnet_max_ip_count
268
return ip_list
269
270
271
def mac_from_ip(ip_address):
272
"""Create a MAC address based on the provided IP.
273
274
Algorithm:
275
- the first 2 bytes are fixed to 06:00
276
- the next 4 bytes are the IP address
277
278
Example of function call:
279
mac_from_ip("192.168.241.2") -> 06:00:C0:A8:F1:02
280
C0 = 192, A8 = 168, F1 = 241 and 02 = 2
281
:param ip_address: IP address as string
282
:return: MAC address from IP
283
"""
284
mac_as_list = ['06', '00']
285
mac_as_list.extend(
286
list(
287
map(
288
lambda val: '{0:02x}'.format(int(val)),
289
ip_address.split('.')
290
)
291
)
292
)
293
294
return "{}:{}:{}:{}:{}:{}".format(*mac_as_list)
295
296
297
def get_guest_net_if_name(ssh_connection, guest_ip):
298
"""Get network interface name based on its IPv4 address."""
299
cmd = "ip a s | grep '{}' | tr -s ' ' | cut -d' ' -f6".format(guest_ip)
300
_, guest_if_name, _ = ssh_connection.execute_command(cmd)
301
if_name = guest_if_name.read().strip()
302
return if_name if if_name != '' else None
303
304
305
class Tap:
306
"""Functionality for creating a tap and cleaning up after it."""
307
308
def __init__(self, name, netns, ip=None):
309
"""Set up the name and network namespace for this tap interface.
310
311
It also creates a new tap device, and brings it up. The tap will
312
stay on the host as long as the object obtained by instantiating this
313
class will be in scope. Once it goes out of scope, its destructor will
314
get called and the tap interface will get removed.
315
The function also moves the interface to the specified
316
namespace.
317
"""
318
utils.run_cmd('ip tuntap add mode tap name ' + name)
319
utils.run_cmd('ip link set {} netns {}'.format(name, netns))
320
if ip:
321
utils.run_cmd('ip netns exec {} ifconfig {} {} up'.format(
322
netns,
323
name,
324
ip
325
))
326
self._name = name
327
self._netns = netns
328
329
@property
330
def name(self):
331
"""Return the name of this tap interface."""
332
return self._name
333
334
@property
335
def netns(self):
336
"""Return the network namespace of this tap."""
337
return self._netns
338
339
def set_tx_queue_len(self, tx_queue_len):
340
"""Set the length of the tap's TX queue."""
341
utils.run_cmd(
342
'ip netns exec {} ip link set {} txqueuelen {}'.format(
343
self.netns,
344
self.name,
345
tx_queue_len
346
)
347
)
348
349