From 302a5cebe4cfa9411a9d27c5ee0cc7122b073333 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Wed, 10 May 2023 12:59:33 +0200 Subject: [PATCH 01/18] libnm-core: add internal _nm_ip_route_ref() helper For some reason, nm_ip_route_ref() does not return the referenced instance, making it cumbersome to use. Add a helper. --- src/libnm-core-aux-intern/nm-libnm-core-utils.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libnm-core-aux-intern/nm-libnm-core-utils.h b/src/libnm-core-aux-intern/nm-libnm-core-utils.h index 589291e5a..b1336731b 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.h +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.h @@ -21,6 +21,13 @@ #define nm_auto_unref_ip_address nm_auto(_nm_ip_address_unref) NM_AUTO_DEFINE_FCN0(NMIPAddress *, _nm_ip_address_unref, nm_ip_address_unref); +static inline NMIPRoute * +_nm_ip_route_ref(NMIPRoute *route) +{ + nm_ip_route_ref(route); + return route; +} + #define nm_auto_unref_ip_route nm_auto(_nm_auto_unref_ip_route) NM_AUTO_DEFINE_FCN0(NMIPRoute *, _nm_auto_unref_ip_route, nm_ip_route_unref); From 0888ed93f785efff5b53e55ff27499ae69ec9640 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Wed, 10 May 2023 12:48:22 +0200 Subject: [PATCH 02/18] cloud-init: fix leaking iproutes for GCP provider The routes in iproutes were leaked (and ownership stolen in _nmc_mangle_connection(), leaving dangling pointers). Fix that by using a GPtrArray instead. --- src/nm-cloud-setup/main.c | 9 +++++---- src/nm-cloud-setup/nmcs-provider-gcp.c | 9 ++++----- src/nm-cloud-setup/nmcs-provider.c | 2 +- src/nm-cloud-setup/nmcs-provider.h | 7 ++++--- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/nm-cloud-setup/main.c b/src/nm-cloud-setup/main.c index d6802634b..5efa87bd3 100644 --- a/src/nm-cloud-setup/main.c +++ b/src/nm-cloud-setup/main.c @@ -315,8 +315,9 @@ _nmc_mangle_connection(NMDevice *device, addrs_new = g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_address_unref); rules_new = g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_routing_rule_unref); - routes_new = g_ptr_array_new_full(config_data->iproutes_len + !!config_data->ipv4s_len, - (GDestroyNotify) nm_ip_route_unref); + routes_new = + g_ptr_array_new_full(nm_g_ptr_array_len(config_data->iproutes) + !!config_data->ipv4s_len, + (GDestroyNotify) nm_ip_route_unref); if (remote_s_ip) { guint len; @@ -422,8 +423,8 @@ _nmc_mangle_connection(NMDevice *device, } } - for (i = 0; i < config_data->iproutes_len; ++i) - g_ptr_array_add(routes_new, config_data->iproutes_arr[i]); + for (i = 0; i < nm_g_ptr_array_len(config_data->iproutes); i++) + g_ptr_array_add(routes_new, _nm_ip_route_ref(config_data->iproutes->pdata[i])); addrs_changed = nmcs_setting_ip_replace_ipv4_addresses(s_ip, (NMIPAddress **) addrs_new->pdata, diff --git a/src/nm-cloud-setup/nmcs-provider-gcp.c b/src/nm-cloud-setup/nmcs-provider-gcp.c index ca354865d..d1d4d821c 100644 --- a/src/nm-cloud-setup/nmcs-provider-gcp.c +++ b/src/nm-cloud-setup/nmcs-provider-gcp.c @@ -112,7 +112,6 @@ _get_config_fip_cb(GObject *source, GAsyncResult *result, gpointer user_data) GCPIfaceData *iface_data = user_data; gs_free_error GError *error = NULL; gs_free char *ipaddr = NULL; - NMIPRoute **routes_arr; NMIPRoute *route_new; nm_http_client_poll_req_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); @@ -137,15 +136,14 @@ _get_config_fip_cb(GObject *source, GAsyncResult *result, gpointer user_data) ipaddr); iface_get_config = iface_data->iface_get_config; - routes_arr = iface_get_config->iproutes_arr; route_new = nm_ip_route_new(AF_INET, ipaddr, 32, NULL, 100, &error); if (error) goto out_done; nm_ip_route_set_attribute(route_new, NM_IP_ROUTE_ATTRIBUTE_TYPE, g_variant_new_string("local")); - routes_arr[iface_get_config->iproutes_len] = route_new; - ++iface_get_config->iproutes_len; + + g_ptr_array_add(iface_get_config->iproutes, route_new); out_done: if (!error) { @@ -215,7 +213,8 @@ _get_config_ips_list_cb(GObject *source, GAsyncResult *result, gpointer user_dat goto out_error; } - iface_data->iface_get_config->iproutes_arr = g_new(NMIPRoute *, iface_data->n_fips_pending); + iface_data->iface_get_config->iproutes = + g_ptr_array_new_full(iface_data->n_fips_pending, (GDestroyNotify) nm_ip_route_unref); for (i = 0; i < uri_arr->len; ++i) { const char *str = uri_arr->pdata[i]; diff --git a/src/nm-cloud-setup/nmcs-provider.c b/src/nm-cloud-setup/nmcs-provider.c index fd9a61b81..d785fd23c 100644 --- a/src/nm-cloud-setup/nmcs-provider.c +++ b/src/nm-cloud-setup/nmcs-provider.c @@ -216,7 +216,7 @@ _iface_data_free(gpointer data) NMCSProviderGetConfigIfaceData *iface_data = data; g_free(iface_data->ipv4s_arr); - g_free(iface_data->iproutes_arr); + nm_g_ptr_array_unref(iface_data->iproutes); g_free((char *) iface_data->hwaddr); nm_g_slice_free(iface_data); diff --git a/src/nm-cloud-setup/nmcs-provider.h b/src/nm-cloud-setup/nmcs-provider.h index 09cdb4143..9e5eeebe6 100644 --- a/src/nm-cloud-setup/nmcs-provider.h +++ b/src/nm-cloud-setup/nmcs-provider.h @@ -34,8 +34,8 @@ typedef struct { bool has_cidr : 1; bool has_gateway : 1; - NMIPRoute **iproutes_arr; - gsize iproutes_len; + /* Array of NMIPRoute (must own/free the entries). */ + GPtrArray *iproutes; /* TRUE, if the configuration was requested via hwaddrs argument to * nmcs_provider_get_config(). */ @@ -59,7 +59,8 @@ static inline gboolean nmcs_provider_get_config_iface_data_is_valid(const NMCSProviderGetConfigIfaceData *config_data) { return config_data && config_data->iface_idx >= 0 - && ((config_data->has_ipv4s && config_data->has_cidr) || config_data->iproutes_len); + && ((config_data->has_ipv4s && config_data->has_cidr) + || nm_g_ptr_array_len(config_data->iproutes) > 0); } /*****************************************************************************/ From cb51aee21cb06488a410fe0a380504fd7615d4ad Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Thu, 11 May 2023 08:26:37 +0200 Subject: [PATCH 03/18] test-client: increase context in pexecpt failure for debugging When a pexpect check fails, we want to see the full content of the buffer, so we can better see where it went wrong. Increase the context that is printed in the error message. --- src/tests/client/test-client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index b6f7cf00a..e33317b1a 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -1009,6 +1009,8 @@ class TestNmClient(unittest.TestCase): pexp = pexpect.spawn(argv[0], argv[1:], timeout=10, env=env) + pexp.str_last_chars = 100000 + typ = collections.namedtuple("CallPexpect", ["pexp", "valgrind_log"]) return typ(pexp, valgrind_log) From 751ee63e61535bceda86886e6f1bce40059c33e5 Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Thu, 11 May 2023 08:40:17 +0200 Subject: [PATCH 04/18] test-client: cleanup after test on failure Otherwise, the following tests will fail too. --- src/tests/client/test-client.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index e33317b1a..dd13c186d 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2175,14 +2175,22 @@ class TestNmCloudSetup(TestNmClient): self.md_url = "http://%s:%d" % s.getsockname() s.close() + error = None + self.srv_start() - func(self) + try: + func(self) + except Exception as e: + error = e self._nm_test_post() p.stdin.close() p.terminate() p.wait() + if error: + raise error + return f @cloud_setup_test From 2e8ff9f8a095fdc8b6fd3b4f0adfe880c9eb8c3a Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Fri, 21 Apr 2023 14:17:48 +0200 Subject: [PATCH 05/18] Revert "client/tests: don't do dup2() dance to pass file descriptor to "tools/test-cloud-meta-mock.py"" This changed the fd passing protocol making it not compatible with systemd-socket-activate(1). This reverts commit 342ee618c75b350cf5cccf49f2bade85c5dfa3ea. --- src/tests/client/test-client.py | 6 +++++- tools/test-cloud-meta-mock.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index dd13c186d..eadc2d496 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2162,14 +2162,18 @@ class TestNmCloudSetup(TestNmClient): # hallucinogenic substances. s.listen(5) + def pass_socket(): + os.dup2(s.fileno(), 3) + service_path = PathConfiguration.test_cloud_meta_mock_path() env = os.environ.copy() - env["LISTEN_FD"] = str(s.fileno()) + env["LISTEN_FDS"] = "1" p = subprocess.Popen( [sys.executable, service_path], stdin=subprocess.PIPE, env=env, pass_fds=(s.fileno(),), + preexec_fn=pass_socket, ) self.md_url = "http://%s:%d" % s.getsockname() diff --git a/tools/test-cloud-meta-mock.py b/tools/test-cloud-meta-mock.py index 392955b8a..262dc2ffb 100755 --- a/tools/test-cloud-meta-mock.py +++ b/tools/test-cloud-meta-mock.py @@ -68,9 +68,11 @@ class SocketHTTPServer(HTTPServer): # See sd_listen_fds(3) -fileno = os.getenv("LISTEN_FD") +fileno = os.getenv("LISTEN_FDS") if fileno is not None: - s = socket.socket(fileno=int(fileno)) + if fileno != "1": + raise Exception("Bad LISTEN_FDS") + s = socket.socket(fileno=3) else: addr = ("localhost", 0) s = socket.socket() From 63452e886f62c8051f29a2130bac772474cdbe0f Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Thu, 13 Apr 2023 13:06:38 +0200 Subject: [PATCH 06/18] test: fix file description passing to cloud-setup mock service The pass_fds file descriptor is *after* the dup2. Always 3. --- src/tests/client/test-client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index eadc2d496..3077f21e3 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2172,7 +2172,7 @@ class TestNmCloudSetup(TestNmClient): [sys.executable, service_path], stdin=subprocess.PIPE, env=env, - pass_fds=(s.fileno(),), + pass_fds=(3,), preexec_fn=pass_socket, ) From 066720991386bb323a624cd55b2bcc41627593c2 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:05:02 +0200 Subject: [PATCH 07/18] test-client: hardcode the cloud-setup mac addresses We rely on the predictable but random MAC addresses. Hardcode them instead -- the mock service also hardcodes them. --- src/tests/client/test-client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index 3077f21e3..f5989d49c 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2201,7 +2201,7 @@ class TestNmCloudSetup(TestNmClient): def test_ec2(self): # Add a device with an active connection that has IPv4 configured - self.srv.op_AddObj("WiredDevice", iface="eth0") + self.srv.op_AddObj("WiredDevice", iface="eth0", mac="9e:c0:3e:92:24:2d") self.srv.addAndActivateConnection( { "connection": {"type": "802-3-ethernet", "id": "con-eth0"}, @@ -2212,7 +2212,7 @@ class TestNmCloudSetup(TestNmClient): ) # The second connection has no IPv4 - self.srv.op_AddObj("WiredDevice", iface="eth1") + self.srv.op_AddObj("WiredDevice", iface="eth1", mac="53:e9:7e:52:8d:a8") self.srv.addAndActivateConnection( {"connection": {"type": "802-3-ethernet", "id": "con-eth1"}}, "/org/freedesktop/NetworkManager/Devices/2", From e56df68464cacc28553f515e1d2edcdfa690ead8 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:05:44 +0200 Subject: [PATCH 08/18] test-client: factor out the test device setup We're going to reuse the setup for tests of other cloud providers. --- src/tests/client/test-client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index f5989d49c..a49317186 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2197,9 +2197,7 @@ class TestNmCloudSetup(TestNmClient): return f - @cloud_setup_test - def test_ec2(self): - + def _mock_devices(self): # Add a device with an active connection that has IPv4 configured self.srv.op_AddObj("WiredDevice", iface="eth0", mac="9e:c0:3e:92:24:2d") self.srv.addAndActivateConnection( From 41f0f6fec825fc5f4be11ca0ba314a7d9aa8e2fd Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Thu, 20 Apr 2023 08:27:04 +0200 Subject: [PATCH 09/18] test/cloud-meta-mock: allow putting the resources This reworks the cloud metadata mock server in a significant way. Most importantly this makes it possible for the client to add and modify the resources for later retrieval using the PUT method. This allows the test to create the fixture for itself. The default set of resources is still provided, so that the too remains useful as a development aid. If that is not desirable, the --empty parameter might be passed to cause the server to start with no resources. --- tools/test-cloud-meta-mock.py | 74 +++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/tools/test-cloud-meta-mock.py b/tools/test-cloud-meta-mock.py index 262dc2ffb..5c4fcad4a 100755 --- a/tools/test-cloud-meta-mock.py +++ b/tools/test-cloud-meta-mock.py @@ -1,13 +1,23 @@ #!/usr/bin/env python +# A service that mocks up various metadata providers. Used for testing, +# can also be used standalone as a development aid. +# +# To run standalone: +# # run: $ systemd-socket-activate -l 8000 python tools/test-cloud-meta-mock.py & # $ NM_CLOUD_SETUP_EC2_HOST=http://localhost:8000 \ # NM_CLOUD_SETUP_LOG=trace \ # NM_CLOUD_SETUP_EC2=yes src/nm-cloud-setup/nm-cloud-setup # or just: $ python tools/test-cloud-meta-mock.py +# +# By default, the utility will server some resources for each known cloud +# providers, for convenience. The tests start this with "--empty" argument, +# which starts with no resources. import os import socket +from sys import argv from http.server import HTTPServer from http.server import BaseHTTPRequestHandler @@ -20,35 +30,39 @@ class MockCloudMDRequestHandler(BaseHTTPRequestHandler): Currently implements a fairly minimal subset of AWS EC2 API. """ - _ec2_macs = "/2018-09-24/meta-data/network/interfaces/macs/" - _meta_resources = { - "/latest/meta-data/": b"ami-id\n", - _ec2_macs: b"9e:c0:3e:92:24:2d\n53:e9:7e:52:8d:a8", - _ec2_macs + "9e:c0:3e:92:24:2d/subnet-ipv4-cidr-block": b"172.31.16.0/20", - _ec2_macs + "9e:c0:3e:92:24:2d/local-ipv4s": b"172.31.26.249", - _ec2_macs + "53:e9:7e:52:8d:a8/subnet-ipv4-cidr-block": b"172.31.166.0/20", - _ec2_macs + "53:e9:7e:52:8d:a8/local-ipv4s": b"172.31.176.249", - } - def log_message(self, format, *args): pass def do_GET(self): - if self.path in self._meta_resources: + path = self.path.encode("ascii") + if path in self.server._resources: self.send_response(200) self.end_headers() - self.wfile.write(self._meta_resources[self.path]) + self.wfile.write(self.server._resources[path]) else: self.send_response(404) self.end_headers() def do_PUT(self): - if self.path == "/latest/api/token": + path = self.path.encode("ascii") + if path == b"/latest/api/token": self.send_response(200) self.end_headers() self.wfile.write( b"AQAAALH-k7i18JMkK-ORLZQfAa7nkNjQbKwpQPExNHqzk1oL_7eh-A==" ) + else: + length = int(self.headers["content-length"]) + self.server._resources[path] = self.rfile.read(length) + self.send_response(201) + self.end_headers() + + def do_DELETE(self): + path = self.path.encode("ascii") + if path in self.server._resources: + del self.server._resources[path] + self.send_response(204) + self.end_headers() else: self.send_response(404) self.end_headers() @@ -61,12 +75,41 @@ class SocketHTTPServer(HTTPServer): fron the test runner. """ - def __init__(self, server_address, RequestHandlerClass, socket): + def __init__(self, server_address, RequestHandlerClass, socket, resources): BaseServer.__init__(self, server_address, RequestHandlerClass) self.socket = socket self.server_address = self.socket.getsockname() + self._resources = resources +def default_resources(): + ec2_macs = b"/2018-09-24/meta-data/network/interfaces/macs/" + + mac1 = b"9e:c0:3e:92:24:2d" + mac2 = b"53:e9:7e:52:8d:a8" + + ip1 = b"172.31.26.249" + ip2 = b"172.31.176.249" + + return { + b"/latest/meta-data/": b"ami-id\n", + ec2_macs: mac2 + b"\n" + mac1, + ec2_macs + mac2 + b"/subnet-ipv4-cidr-block": b"172.31.16.0/20", + ec2_macs + mac2 + b"/local-ipv4s": ip1, + ec2_macs + mac1 + b"/subnet-ipv4-cidr-block": b"172.31.166.0/20", + ec2_macs + mac1 + b"/local-ipv4s": ip2, + } + + +resources = None +try: + if argv[1] == "--empty": + resources = {} +except IndexError: + pass +if resources is None: + resources = default_resources() + # See sd_listen_fds(3) fileno = os.getenv("LISTEN_FDS") if fileno is not None: @@ -80,8 +123,7 @@ else: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) s.bind(addr) - -httpd = SocketHTTPServer(None, MockCloudMDRequestHandler, socket=s) +httpd = SocketHTTPServer(None, MockCloudMDRequestHandler, socket=s, resources=resources) print("Listening on http://%s:%d" % (httpd.server_address[0], httpd.server_address[1])) httpd.server_activate() From 6a1dd3b0f818ddc5bf60d30bfa6bcc359323e4f6 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Thu, 20 Apr 2023 08:08:15 +0200 Subject: [PATCH 10/18] test-client: use a test fixture from the test Don't rely on resources provided by mock metadata server by default, create the from within the test instead. This allows for more flexibility, but the locality of the test fixture relative to the tests makes the test more legible. --- src/tests/client/test-client.py | 43 +++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index a49317186..4013340fe 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -147,6 +147,7 @@ except ImportError: try: from http.server import HTTPServer from http.server import BaseHTTPRequestHandler + from http.client import HTTPConnection, HTTPResponse except ImportError: HTTPServer = None @@ -2139,6 +2140,13 @@ class TestNmcli(TestNmClient): class TestNmCloudSetup(TestNmClient): + + _mac1 = "9e:c0:3e:92:24:2d" + _mac2 = "53:e9:7e:52:8d:a8" + + _ip1 = "172.31.26.249" + _ip2 = "172.31.176.249" + def cloud_setup_test(func): """ Runs the mock NetworkManager along with a mock cloud metadata service. @@ -2169,14 +2177,16 @@ class TestNmCloudSetup(TestNmClient): env = os.environ.copy() env["LISTEN_FDS"] = "1" p = subprocess.Popen( - [sys.executable, service_path], + [sys.executable, service_path, "--empty"], stdin=subprocess.PIPE, env=env, pass_fds=(3,), preexec_fn=pass_socket, ) - self.md_url = "http://%s:%d" % s.getsockname() + (hostaddr, port) = s.getsockname() + self.md_conn = HTTPConnection(hostaddr, port=port) + self.md_url = "http://%s:%d" % (hostaddr, port) s.close() error = None @@ -2188,6 +2198,7 @@ class TestNmCloudSetup(TestNmClient): error = e self._nm_test_post() + self.md_conn.close() p.stdin.close() p.terminate() p.wait() @@ -2218,6 +2229,34 @@ class TestNmCloudSetup(TestNmClient): delay=0, ) + def _mock_path(self, path, body): + self.md_conn.request("PUT", path, body=body) + self.md_conn.getresponse().read() + + @cloud_setup_test + def test_ec2(self): + self._mock_devices() + + _ec2_macs = "/2018-09-24/meta-data/network/interfaces/macs/" + self._mock_path("/latest/meta-data/", "ami-id\n") + self._mock_path( + _ec2_macs, TestNmCloudSetup._mac2 + "\n" + TestNmCloudSetup._mac1 + ) + self._mock_path( + _ec2_macs + TestNmCloudSetup._mac2 + "/subnet-ipv4-cidr-block", + "172.31.16.0/20", + ) + self._mock_path( + _ec2_macs + TestNmCloudSetup._mac2 + "/local-ipv4s", TestNmCloudSetup._ip1 + ) + self._mock_path( + _ec2_macs + TestNmCloudSetup._mac1 + "/subnet-ipv4-cidr-block", + "172.31.166.0/20", + ) + self._mock_path( + _ec2_macs + TestNmCloudSetup._mac1 + "/local-ipv4s", TestNmCloudSetup._ip2 + ) + # Run nm-cloud-setup for the first time nmc = self.call_pexpect( ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, From 1f310abfd81f8f4fac8a2e761fe6aa6ae5e755d5 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:07:49 +0200 Subject: [PATCH 11/18] test/client: test cloud-setup aliyun support --- src/tests/client/test-client.py | 90 +++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index 4013340fe..d48d2e4ec 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2233,6 +2233,96 @@ class TestNmCloudSetup(TestNmClient): self.md_conn.request("PUT", path, body=body) self.md_conn.getresponse().read() + @cloud_setup_test + def test_aliyun(self): + self._mock_devices() + + _aliyun_meta = "/2016-01-01/meta-data/" + _aliyun_macs = _aliyun_meta + "network/interfaces/macs/" + self._mock_path(_aliyun_meta, "ami-id\n") + self._mock_path( + _aliyun_macs, TestNmCloudSetup._mac2 + "\n" + TestNmCloudSetup._mac1 + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac2 + "/vpc-cidr-block", "172.31.16.0/20" + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac2 + "/private-ipv4s", + TestNmCloudSetup._ip1, + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac2 + "/primary-ip-address", + TestNmCloudSetup._ip1, + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac2 + "/netmask", "255.255.255.0" + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac2 + "/gateway", "172.31.26.2" + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac1 + "/vpc-cidr-block", "172.31.166.0/20" + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac1 + "/private-ipv4s", + TestNmCloudSetup._ip2, + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac1 + "/primary-ip-address", + TestNmCloudSetup._ip2, + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac1 + "/netmask", "255.255.255.0" + ) + self._mock_path( + _aliyun_macs + TestNmCloudSetup._mac1 + "/gateway", "172.31.176.2" + ) + + # Run nm-cloud-setup for the first time + nmc = self.call_pexpect( + ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, + [], + { + "NM_CLOUD_SETUP_ALIYUN_HOST": self.md_url, + "NM_CLOUD_SETUP_LOG": "trace", + "NM_CLOUD_SETUP_ALIYUN": "yes", + }, + ) + + nmc.pexp.expect("provider aliyun detected") + nmc.pexp.expect("found interfaces: 9E:C0:3E:92:24:2D, 53:E9:7E:52:8D:A8") + nmc.pexp.expect("get-config: start fetching meta data") + nmc.pexp.expect("get-config: success") + nmc.pexp.expect("meta data received") + # One of the devices has no IPv4 configuration to be modified + nmc.pexp.expect("device has no suitable applied connection. Skip") + # The other one was lacking an address set it up. + nmc.pexp.expect("some changes were applied for provider aliyun") + nmc.pexp.expect(pexpect.EOF) + + # Run nm-cloud-setup for the second time + nmc = self.call_pexpect( + ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, + [], + { + "NM_CLOUD_SETUP_ALIYUN_HOST": self.md_url, + "NM_CLOUD_SETUP_LOG": "trace", + "NM_CLOUD_SETUP_ALIYUN": "yes", + }, + ) + + nmc.pexp.expect("provider aliyun detected") + nmc.pexp.expect("found interfaces: 9E:C0:3E:92:24:2D, 53:E9:7E:52:8D:A8") + nmc.pexp.expect("get-config: starting") + nmc.pexp.expect("get-config: success") + nmc.pexp.expect("meta data received") + # No changes this time + nmc.pexp.expect('device needs no update to applied connection "con-eth0"') + nmc.pexp.expect("no changes were applied for provider aliyun") + nmc.pexp.expect(pexpect.EOF) + + Util.valgrind_check_log(nmc.valgrind_log, "test_aliyun") + @cloud_setup_test def test_ec2(self): self._mock_devices() From 515e69df3a549aa5a23052f3d68a04e1ed1e6b99 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:08:12 +0200 Subject: [PATCH 12/18] cloud-setup/azure: add ability to redirect metadata API requests A different host can be specified with (undocumented, private) NM_CLOUD_SETUP_AZURE_HOST environment variable. --- src/nm-cloud-setup/nmcs-provider-azure.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/nm-cloud-setup/nmcs-provider-azure.c b/src/nm-cloud-setup/nmcs-provider-azure.c index 69946f5c5..418d380a5 100644 --- a/src/nm-cloud-setup/nmcs-provider-azure.c +++ b/src/nm-cloud-setup/nmcs-provider-azure.c @@ -17,8 +17,30 @@ #define NM_AZURE_METADATA_URL_BASE /* $NM_AZURE_BASE/$NM_AZURE_API_VERSION */ \ "/metadata/instance/network/interface/" +static const char * +_azure_base(void) +{ + static const char *base_cached = NULL; + const char *base; + +again: + base = g_atomic_pointer_get(&base_cached); + if (G_UNLIKELY(!base)) { + /* The base URI can be set via environment variable. + * This is mainly for testing, it's not usually supposed to be configured. + * Consider this private API! */ + base = g_getenv(NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_AZURE_HOST")); + base = nmcs_utils_uri_complete_interned(base) ?: ("" NM_AZURE_BASE); + + if (!g_atomic_pointer_compare_and_exchange(&base_cached, NULL, base)) + goto again; + } + + return base; +} + #define _azure_uri_concat(...) \ - nmcs_utils_uri_build_concat(NM_AZURE_BASE, __VA_ARGS__, NM_AZURE_API_VERSION) + nmcs_utils_uri_build_concat(_azure_base(), __VA_ARGS__, NM_AZURE_API_VERSION) #define _azure_uri_interfaces(...) _azure_uri_concat(NM_AZURE_METADATA_URL_BASE, ##__VA_ARGS__) /*****************************************************************************/ From 661545efcd65b085a76494ab58f1565b0c0bd5ab Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:07:56 +0200 Subject: [PATCH 13/18] test/client: test cloud-setup azure support --- src/tests/client/test-client.py | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index d48d2e4ec..91a555e64 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2323,6 +2323,92 @@ class TestNmCloudSetup(TestNmClient): Util.valgrind_check_log(nmc.valgrind_log, "test_aliyun") + @cloud_setup_test + def test_azure(self): + self._mock_devices() + + _azure_meta = "/metadata/instance" + _azure_iface = _azure_meta + "/network/interface/" + _azure_query = "?format=text&api-version=2017-04-02" + self._mock_path(_azure_meta + _azure_query, "") + self._mock_path(_azure_iface + _azure_query, "0\n1\n") + self._mock_path( + _azure_iface + "0/macAddress" + _azure_query, TestNmCloudSetup._mac1 + ) + self._mock_path( + _azure_iface + "1/macAddress" + _azure_query, TestNmCloudSetup._mac2 + ) + self._mock_path(_azure_iface + "0/ipv4/ipAddress/" + _azure_query, "0\n") + self._mock_path(_azure_iface + "1/ipv4/ipAddress/" + _azure_query, "0\n") + self._mock_path( + _azure_iface + "0/ipv4/ipAddress/0/privateIpAddress" + _azure_query, + TestNmCloudSetup._ip1, + ) + self._mock_path( + _azure_iface + "1/ipv4/ipAddress/0/privateIpAddress" + _azure_query, + TestNmCloudSetup._ip2, + ) + self._mock_path( + _azure_iface + "0/ipv4/subnet/0/address/" + _azure_query, "172.31.16.0" + ) + self._mock_path( + _azure_iface + "1/ipv4/subnet/0/address/" + _azure_query, "172.31.166.0" + ) + self._mock_path(_azure_iface + "0/ipv4/subnet/0/prefix/" + _azure_query, "20") + self._mock_path(_azure_iface + "1/ipv4/subnet/0/prefix/" + _azure_query, "20") + + # Run nm-cloud-setup for the first time + nmc = self.call_pexpect( + ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, + [], + { + "NM_CLOUD_SETUP_AZURE_HOST": self.md_url, + "NM_CLOUD_SETUP_LOG": "trace", + "NM_CLOUD_SETUP_AZURE": "yes", + }, + ) + + nmc.pexp.expect("provider azure detected") + nmc.pexp.expect("found interfaces: 9E:C0:3E:92:24:2D, 53:E9:7E:52:8D:A8") + nmc.pexp.expect("found azure interfaces: 2") + nmc.pexp.expect("interface\[0]: found a matching device with hwaddr") + nmc.pexp.expect( + "interface\[0]: (received subnet address|received subnet prefix 20)" + ) + nmc.pexp.expect( + "interface\[0]: (received subnet address|received subnet prefix 20)" + ) + nmc.pexp.expect("get-config: success") + nmc.pexp.expect("meta data received") + # One of the devices has no IPv4 configuration to be modified + nmc.pexp.expect("device has no suitable applied connection. Skip") + # The other one was lacking an address set it up. + nmc.pexp.expect("some changes were applied for provider azure") + nmc.pexp.expect(pexpect.EOF) + + # Run nm-cloud-setup for the second time + nmc = self.call_pexpect( + ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, + [], + { + "NM_CLOUD_SETUP_AZURE_HOST": self.md_url, + "NM_CLOUD_SETUP_LOG": "trace", + "NM_CLOUD_SETUP_AZURE": "yes", + }, + ) + + nmc.pexp.expect("provider azure detected") + nmc.pexp.expect("found interfaces: 9E:C0:3E:92:24:2D, 53:E9:7E:52:8D:A8") + nmc.pexp.expect("get-config: starting") + nmc.pexp.expect("get-config: success") + nmc.pexp.expect("meta data received") + # No changes this time + nmc.pexp.expect('device needs no update to applied connection "con-eth0"') + nmc.pexp.expect("no changes were applied for provider azure") + nmc.pexp.expect(pexpect.EOF) + + Util.valgrind_check_log(nmc.valgrind_log, "test_azure") + @cloud_setup_test def test_ec2(self): self._mock_devices() From 79f6a7da567637443f82d2ea1d6ea10e8f3c6208 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:08:05 +0200 Subject: [PATCH 14/18] cloud-setup/gcp: add ability to redirect metadata API requests A different host can be specified with (undocumented, private) NM_CLOUD_SETUP_GCP_HOST environment variable. --- src/nm-cloud-setup/nmcs-provider-gcp.c | 38 +++++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/nm-cloud-setup/nmcs-provider-gcp.c b/src/nm-cloud-setup/nmcs-provider-gcp.c index d1d4d821c..33d0fe08f 100644 --- a/src/nm-cloud-setup/nmcs-provider-gcp.c +++ b/src/nm-cloud-setup/nmcs-provider-gcp.c @@ -13,15 +13,39 @@ #define HTTP_POLL_TIMEOUT_MS 10000 #define HTTP_RATE_LIMIT_MS 1000 -#define NM_GCP_HOST "metadata.google.internal" -#define NM_GCP_BASE "http://" NM_GCP_HOST -#define NM_GCP_API_VERSION "/v1" -#define NM_GCP_METADATA_URL_BASE NM_GCP_BASE "/computeMetadata" NM_GCP_API_VERSION "/instance" -#define NM_GCP_METADATA_URL_NET "/network-interfaces/" +#define NM_GCP_HOST "metadata.google.internal" +#define NM_GCP_BASE "http://" NM_GCP_HOST +#define NM_GCP_API_VERSION "/v1" +#define NM_GCP_METADATA_URL_NET "/network-interfaces/" #define NM_GCP_METADATA_HEADER "Metadata-Flavor: Google" -#define _gcp_uri_concat(...) nmcs_utils_uri_build_concat(NM_GCP_METADATA_URL_BASE, __VA_ARGS__) +static const char * +_gcp_base(void) +{ + static const char *base_cached = NULL; + const char *base; + +again: + base = g_atomic_pointer_get(&base_cached); + if (G_UNLIKELY(!base)) { + /* The base URI can be set via environment variable. + * This is mainly for testing, it's not usually supposed to be configured. + * Consider this private API! */ + base = g_getenv(NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_GCP_HOST")); + base = nmcs_utils_uri_complete_interned(base) ?: ("" NM_GCP_BASE); + + if (!g_atomic_pointer_compare_and_exchange(&base_cached, NULL, base)) + goto again; + } + + return base; +} + +#define _gcp_uri_concat(...) \ + nmcs_utils_uri_build_concat(_gcp_base(), \ + "/computeMetadata" NM_GCP_API_VERSION "/instance", \ + __VA_ARGS__) #define _gcp_uri_interfaces(...) _gcp_uri_concat(NM_GCP_METADATA_URL_NET, ##__VA_ARGS__) /*****************************************************************************/ @@ -73,7 +97,7 @@ detect(NMCSProvider *provider, GTask *task) http_client = nmcs_provider_get_http_client(provider); nm_http_client_poll_req(http_client, - (uri = _gcp_uri_concat("id")), + (uri = _gcp_uri_concat("/id")), HTTP_TIMEOUT_MS, 256 * 1024, 7000, From b93ebb9be931d4b6a73fd534f787478ce1c78bcb Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:07:33 +0200 Subject: [PATCH 15/18] test/client: test cloud-setup GCP support --- src/tests/client/test-client.py | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index 91a555e64..dc9efd855 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -2478,6 +2478,67 @@ class TestNmCloudSetup(TestNmClient): Util.valgrind_check_log(nmc.valgrind_log, "test_ec2") + @cloud_setup_test + def test_gcp(self): + self._mock_devices() + + gcp_meta = "/computeMetadata/v1/instance/" + gcp_iface = gcp_meta + "network-interfaces/" + self._mock_path(gcp_meta + "id", "") + self._mock_path(gcp_iface, "0\n1\n") + self._mock_path(gcp_iface + "0/mac", TestNmCloudSetup._mac1) + self._mock_path(gcp_iface + "1/mac", TestNmCloudSetup._mac2) + self._mock_path(gcp_iface + "0/forwarded-ips/", "0\n") + self._mock_path(gcp_iface + "0/forwarded-ips/0", TestNmCloudSetup._ip1) + self._mock_path(gcp_iface + "1/forwarded-ips/", "0\n") + self._mock_path(gcp_iface + "1/forwarded-ips/0", TestNmCloudSetup._ip2) + + # Run nm-cloud-setup for the first time + nmc = self.call_pexpect( + ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, + [], + { + "NM_CLOUD_SETUP_GCP_HOST": self.md_url, + "NM_CLOUD_SETUP_LOG": "trace", + "NM_CLOUD_SETUP_GCP": "yes", + }, + ) + + nmc.pexp.expect("provider GCP detected") + nmc.pexp.expect("found interfaces: 9E:C0:3E:92:24:2D, 53:E9:7E:52:8D:A8") + nmc.pexp.expect("found GCP interfaces: 2") + nmc.pexp.expect("GCP interface\[0]: found a requested device with hwaddr") + nmc.pexp.expect("get-config: success") + nmc.pexp.expect("meta data received") + # One of the devices has no IPv4 configuration to be modified + nmc.pexp.expect("device has no suitable applied connection. Skip") + # The other one was lacking an address set it up. + nmc.pexp.expect("some changes were applied for provider GCP") + nmc.pexp.expect(pexpect.EOF) + + # Run nm-cloud-setup for the second time + nmc = self.call_pexpect( + ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, + [], + { + "NM_CLOUD_SETUP_GCP_HOST": self.md_url, + "NM_CLOUD_SETUP_LOG": "trace", + "NM_CLOUD_SETUP_GCP": "yes", + }, + ) + + nmc.pexp.expect("provider GCP detected") + nmc.pexp.expect("found interfaces: 9E:C0:3E:92:24:2D, 53:E9:7E:52:8D:A8") + nmc.pexp.expect("get-config: starting") + nmc.pexp.expect("get-config: success") + nmc.pexp.expect("meta data received") + # No changes this time + nmc.pexp.expect('device needs no update to applied connection "con-eth0"') + nmc.pexp.expect("no changes were applied for provider GCP") + nmc.pexp.expect(pexpect.EOF) + + Util.valgrind_check_log(nmc.valgrind_log, "test_gcp") + ############################################################################### From 620a737cb7544e1cd91782cc53dba6e3df940bf8 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:09:40 +0200 Subject: [PATCH 16/18] test/cloud-meta-mock: mock aliyun metadata API Not used for testing, but still might be useful for development. --- tools/test-cloud-meta-mock.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/test-cloud-meta-mock.py b/tools/test-cloud-meta-mock.py index 5c4fcad4a..f7eb536f2 100755 --- a/tools/test-cloud-meta-mock.py +++ b/tools/test-cloud-meta-mock.py @@ -85,6 +85,9 @@ class SocketHTTPServer(HTTPServer): def default_resources(): ec2_macs = b"/2018-09-24/meta-data/network/interfaces/macs/" + aliyun_meta = b"/2016-01-01/meta-data/" + aliyun_macs = aliyun_meta + b"network/interfaces/macs/" + mac1 = b"9e:c0:3e:92:24:2d" mac2 = b"53:e9:7e:52:8d:a8" @@ -98,6 +101,18 @@ def default_resources(): ec2_macs + mac2 + b"/local-ipv4s": ip1, ec2_macs + mac1 + b"/subnet-ipv4-cidr-block": b"172.31.166.0/20", ec2_macs + mac1 + b"/local-ipv4s": ip2, + aliyun_meta: b"ami-id\n", + aliyun_macs: mac2 + b"\n" + mac1, + aliyun_macs + mac2 + b"/vpc-cidr-block": b"172.31.16.0/20", + aliyun_macs + mac2 + b"/private-ipv4s": ip1, + aliyun_macs + mac2 + b"/primary-ip-address": ip1, + aliyun_macs + mac2 + b"/netmask": b"255.255.255.0", + aliyun_macs + mac2 + b"/gateway": b"172.31.26.2", + aliyun_macs + mac1 + b"/vpc-cidr-block": b"172.31.166.0/20", + aliyun_macs + mac1 + b"/private-ipv4s": ip2, + aliyun_macs + mac1 + b"/primary-ip-address": ip2, + aliyun_macs + mac1 + b"/netmask": b"255.255.255.0", + aliyun_macs + mac1 + b"/gateway": b"172.31.176.2", } From 5b5ef08aee88a6f30e4ae16b41ce6c9bdc136459 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:09:54 +0200 Subject: [PATCH 17/18] test/cloud-meta-mock: mock azure metadata API Not used for testing, but still might be useful for development. --- tools/test-cloud-meta-mock.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tools/test-cloud-meta-mock.py b/tools/test-cloud-meta-mock.py index f7eb536f2..387b7544f 100755 --- a/tools/test-cloud-meta-mock.py +++ b/tools/test-cloud-meta-mock.py @@ -88,6 +88,10 @@ def default_resources(): aliyun_meta = b"/2016-01-01/meta-data/" aliyun_macs = aliyun_meta + b"network/interfaces/macs/" + azure_meta = b"/metadata/instance" + azure_iface = azure_meta + b"/network/interface/" + azure_query = b"?format=text&api-version=2017-04-02" + mac1 = b"9e:c0:3e:92:24:2d" mac2 = b"53:e9:7e:52:8d:a8" @@ -113,6 +117,18 @@ def default_resources(): aliyun_macs + mac1 + b"/primary-ip-address": ip2, aliyun_macs + mac1 + b"/netmask": b"255.255.255.0", aliyun_macs + mac1 + b"/gateway": b"172.31.176.2", + azure_meta + azure_query: b"", + azure_iface + azure_query: b"0\n1\n", + azure_iface + b"0/macAddress" + azure_query: mac1, + azure_iface + b"1/macAddress" + azure_query: mac2, + azure_iface + b"0/ipv4/ipAddress/" + azure_query: b"0\n", + azure_iface + b"1/ipv4/ipAddress/" + azure_query: b"0\n", + azure_iface + b"0/ipv4/ipAddress/0/privateIpAddress" + azure_query: ip1, + azure_iface + b"1/ipv4/ipAddress/0/privateIpAddress" + azure_query: ip2, + azure_iface + b"0/ipv4/subnet/0/address/" + azure_query: b"172.31.16.0", + azure_iface + b"1/ipv4/subnet/0/address/" + azure_query: b"172.31.166.0", + azure_iface + b"0/ipv4/subnet/0/prefix/" + azure_query: b"20", + azure_iface + b"1/ipv4/subnet/0/prefix/" + azure_query: b"20", } From c72e085f5cb1c10a380aed0969c1503f374667c5 Mon Sep 17 00:00:00 2001 From: Lubomir Rintel Date: Mon, 17 Apr 2023 07:10:06 +0200 Subject: [PATCH 18/18] test/cloud-meta-mock: mock GCP metadata API Not used for testing, but still might be useful for development. --- tools/test-cloud-meta-mock.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/test-cloud-meta-mock.py b/tools/test-cloud-meta-mock.py index 387b7544f..ab3630add 100755 --- a/tools/test-cloud-meta-mock.py +++ b/tools/test-cloud-meta-mock.py @@ -92,6 +92,9 @@ def default_resources(): azure_iface = azure_meta + b"/network/interface/" azure_query = b"?format=text&api-version=2017-04-02" + gcp_meta = b"/computeMetadata/v1/instance/" + gcp_iface = gcp_meta + b"network-interfaces/" + mac1 = b"9e:c0:3e:92:24:2d" mac2 = b"53:e9:7e:52:8d:a8" @@ -129,6 +132,14 @@ def default_resources(): azure_iface + b"1/ipv4/subnet/0/address/" + azure_query: b"172.31.166.0", azure_iface + b"0/ipv4/subnet/0/prefix/" + azure_query: b"20", azure_iface + b"1/ipv4/subnet/0/prefix/" + azure_query: b"20", + gcp_meta + b"id": b"", + gcp_iface: b"0\n1\n", + gcp_iface + b"0/mac": mac1, + gcp_iface + b"1/mac": mac2, + gcp_iface + b"0/forwarded-ips/": b"0\n", + gcp_iface + b"0/forwarded-ips/0": ip1, + gcp_iface + b"1/forwarded-ips/": b"0\n", + gcp_iface + b"1/forwarded-ips/0": ip2, }