diff options
-rw-r--r-- | src/libnm-core-aux-intern/nm-libnm-core-utils.h | 7 | ||||
-rw-r--r-- | src/nm-cloud-setup/main.c | 9 | ||||
-rw-r--r-- | src/nm-cloud-setup/nmcs-provider-azure.c | 24 | ||||
-rw-r--r-- | src/nm-cloud-setup/nmcs-provider-gcp.c | 47 | ||||
-rw-r--r-- | src/nm-cloud-setup/nmcs-provider.c | 2 | ||||
-rw-r--r-- | src/nm-cloud-setup/nmcs-provider.h | 7 | ||||
-rwxr-xr-x | src/tests/client/test-client.py | 779 | ||||
-rwxr-xr-x | tools/test-cloud-meta-mock.py | 122 |
8 files changed, 743 insertions, 254 deletions
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 589291e5a9..b1336731be 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); diff --git a/src/nm-cloud-setup/main.c b/src/nm-cloud-setup/main.c index d6802634bd..5efa87bd3d 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-azure.c b/src/nm-cloud-setup/nmcs-provider-azure.c index 69946f5c53..418d380a5d 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__) /*****************************************************************************/ diff --git a/src/nm-cloud-setup/nmcs-provider-gcp.c b/src/nm-cloud-setup/nmcs-provider-gcp.c index ca354865dd..33d0fe08f9 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, @@ -112,7 +136,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 +160,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 +237,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 fd9a61b813..d785fd23cf 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 09cdb4143d..9e5eeebe69 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); } /*****************************************************************************/ diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index b6f7cf00a4..9aa018b61c 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 @@ -411,6 +412,34 @@ class Util: return None @staticmethod + def file_read_expected(filename): + results_expect = [] + content_expect = Util.file_read(filename) + try: + base_idx = 0 + size_prefix = "size: ".encode("utf8") + while True: + if not content_expect[base_idx : base_idx + 10].startswith(size_prefix): + raise Exception("Unexpected token") + j = base_idx + len(size_prefix) + i = j + if Util.python_has_version(3, 0): + eol = ord("\n") + else: + eol = "\n" + while content_expect[i] != eol: + i += 1 + i = i + 1 + int(content_expect[j:i]) + results_expect.append(content_expect[base_idx:i]) + if len(content_expect) == i: + break + base_idx = i + except Exception as e: + results_expect = None + + return content_expect, results_expect + + @staticmethod def _replace_text_match_join(split_arr, replacement): yield split_arr[0] for t in split_arr[1:]: @@ -540,6 +569,147 @@ class Util: idx = pexp.expect(pattern_list) del pattern_list[idx] + @staticmethod + def skip_without_pexpect(_func=None): + if _func is None: + if pexpect is None: + raise unittest.SkipTest("pexpect not available") + return + + def f(*a, **kw): + Util.skip_without_pexpect() + _func(*a, **kw) + + return f + + @staticmethod + def skip_without_dbus_session(_func=None): + if _func is None: + if not dbus_session_inited: + raise unittest.SkipTest( + "Own D-Bus session for testing is not initialized. Do you have dbus-run-session available?" + ) + return + + def f(*a, **kw): + Util.skip_without_dbus_session() + _func(*a, **kw) + + return f + + @staticmethod + def skip_without_NM(_func=None): + if _func is None: + if NM is None: + raise unittest.SkipTest( + "gi.NM is not available. Did you build with introspection?" + ) + return + + def f(*a, **kw): + Util.skip_without_NM() + _func(*a, **kw) + + return f + + @staticmethod + def cmd_create_env( + lang="C", + calling_num=None, + fatal_warnings=_DEFAULT_ARG, + extra_env=None, + ): + if lang == "C": + language = "" + elif lang == "de_DE.utf8": + language = "de" + elif lang == "pl_PL.UTF-8": + language = "pl" + else: + raise AssertionError("invalid language %s" % (lang)) + + env = {} + for k in [ + "LD_LIBRARY_PATH", + "DBUS_SESSION_BUS_ADDRESS", + "LIBNM_CLIENT_DEBUG", + "LIBNM_CLIENT_DEBUG_FILE", + ]: + val = os.environ.get(k, None) + if val is not None: + env[k] = val + env["LANG"] = lang + env["LANGUAGE"] = language + env["LIBNM_USE_SESSION_BUS"] = "1" + env["LIBNM_USE_NO_UDEV"] = "1" + env["TERM"] = "linux" + env["ASAN_OPTIONS"] = conf.get(ENV_NM_TEST_ASAN_OPTIONS) + env["LSAN_OPTIONS"] = conf.get(ENV_NM_TEST_LSAN_OPTIONS) + env["LBSAN_OPTIONS"] = conf.get(ENV_NM_TEST_UBSAN_OPTIONS) + env["XDG_CONFIG_HOME"] = PathConfiguration.srcdir() + if calling_num is not None: + env["NM_TEST_CALLING_NUM"] = str(calling_num) + if fatal_warnings is _DEFAULT_ARG or fatal_warnings: + env["G_DEBUG"] = "fatal-warnings" + if extra_env is not None: + for k, v in extra_env.items(): + env[k] = v + return env + + @staticmethod + def cmd_create_argv(cmd_path, args, with_valgrind=None): + + if with_valgrind is None: + with_valgrind = conf.get(ENV_NM_TEST_VALGRIND) + + valgrind_log = None + cmd = conf.get(cmd_path) + if with_valgrind: + valgrind_log = tempfile.mkstemp(prefix="nm-test-client-valgrind.") + argv = [ + "valgrind", + "--quiet", + "--error-exitcode=37", + "--leak-check=full", + "--gen-suppressions=all", + ( + "--suppressions=" + + PathConfiguration.top_srcdir() + + "/valgrind.suppressions" + ), + "--num-callers=100", + "--log-file=" + valgrind_log[1], + cmd, + ] + libtool = conf.get(ENV_LIBTOOL) + if libtool: + argv = list(libtool) + ["--mode=execute"] + argv + else: + argv = [cmd] + + argv.extend(args) + return argv, valgrind_log + + @staticmethod + def cmd_call_pexpect(cmd_path, args, extra_env): + argv, valgrind_log = Util.cmd_create_argv(cmd_path, args) + env = Util.cmd_create_env(extra_env=extra_env) + + 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) + + @staticmethod + def cmd_call_pexpect_nmcli(args): + return Util.cmd_call_pexpect( + ENV_NM_TEST_CLIENT_NMCLI_PATH, + args, + {"NO_COLOR": "1"}, + ) + ############################################################################### @@ -778,6 +948,12 @@ class NMStubServer: ) return u + def ReplaceTextConUuid(self, con_name, replacement): + return Util.ReplaceTextSimple( + Util.memoize_nullary(lambda: self.findConnectionUuid(con_name)), + replacement, + ) + def setProperty(self, path, propname, value, iface_name=None): if iface_name is None: iface_name = "" @@ -874,21 +1050,25 @@ class AsyncProcess: ############################################################################### -MAX_JOBS = 15 +class NMTestContext: + MAX_JOBS = 15 - -class TestNmClient(unittest.TestCase): - def __init__(self, *args, **kwargs): + def __init__(self, testMethodName): + self.testMethodName = testMethodName self._calling_num = {} self._skip_test_for_l10n_diff = [] self._async_jobs = [] - self._results = [] + self.ctx_results = [] self.srv = None - unittest.TestCase.__init__(self, *args, **kwargs) + + def calling_num(self, calling_fcn): + calling_num = self._calling_num.get(calling_fcn, 0) + 1 + self._calling_num[calling_fcn] = calling_num + return calling_num def srv_start(self): self.srv_shutdown() - self.srv = NMStubServer(self._testMethodName) + self.srv = NMStubServer(self.testMethodName) def srv_shutdown(self): if self.srv is not None: @@ -896,134 +1076,18 @@ class TestNmClient(unittest.TestCase): self.srv = None srv.shutdown() - def ReplaceTextConUuid(self, con_name, replacement): - return Util.ReplaceTextSimple( - Util.memoize_nullary(lambda: self.srv.findConnectionUuid(con_name)), - replacement, - ) - - @staticmethod - def _read_expected(filename): - results_expect = [] - content_expect = Util.file_read(filename) - try: - base_idx = 0 - size_prefix = "size: ".encode("utf8") - while True: - if not content_expect[base_idx : base_idx + 10].startswith(size_prefix): - raise Exception("Unexpected token") - j = base_idx + len(size_prefix) - i = j - if Util.python_has_version(3, 0): - eol = ord("\n") - else: - eol = "\n" - while content_expect[i] != eol: - i += 1 - i = i + 1 + int(content_expect[j:i]) - results_expect.append(content_expect[base_idx:i]) - if len(content_expect) == i: - break - base_idx = i - except Exception as e: - results_expect = None - - return content_expect, results_expect - - def _env( - self, lang="C", calling_num=None, fatal_warnings=_DEFAULT_ARG, extra_env=None - ): - if lang == "C": - language = "" - elif lang == "de_DE.utf8": - language = "de" - elif lang == "pl_PL.UTF-8": - language = "pl" - else: - self.fail("invalid language %s" % (lang)) - - env = {} - for k in [ - "LD_LIBRARY_PATH", - "DBUS_SESSION_BUS_ADDRESS", - "LIBNM_CLIENT_DEBUG", - "LIBNM_CLIENT_DEBUG_FILE", - ]: - val = os.environ.get(k, None) - if val is not None: - env[k] = val - env["LANG"] = lang - env["LANGUAGE"] = language - env["LIBNM_USE_SESSION_BUS"] = "1" - env["LIBNM_USE_NO_UDEV"] = "1" - env["TERM"] = "linux" - env["ASAN_OPTIONS"] = conf.get(ENV_NM_TEST_ASAN_OPTIONS) - env["LSAN_OPTIONS"] = conf.get(ENV_NM_TEST_LSAN_OPTIONS) - env["LBSAN_OPTIONS"] = conf.get(ENV_NM_TEST_UBSAN_OPTIONS) - env["XDG_CONFIG_HOME"] = PathConfiguration.srcdir() - if calling_num is not None: - env["NM_TEST_CALLING_NUM"] = str(calling_num) - if fatal_warnings is _DEFAULT_ARG or fatal_warnings: - env["G_DEBUG"] = "fatal-warnings" - if extra_env is not None: - for k, v in extra_env.items(): - env[k] = v - return env - - def cmd_construct_argv(self, cmd_path, args, with_valgrind=None): - - if with_valgrind is None: - with_valgrind = conf.get(ENV_NM_TEST_VALGRIND) - - valgrind_log = None - cmd = conf.get(cmd_path) - if with_valgrind: - valgrind_log = tempfile.mkstemp(prefix="nm-test-client-valgrind.") - argv = [ - "valgrind", - "--quiet", - "--error-exitcode=37", - "--leak-check=full", - "--gen-suppressions=all", - ( - "--suppressions=" - + PathConfiguration.top_srcdir() - + "/valgrind.suppressions" - ), - "--num-callers=100", - "--log-file=" + valgrind_log[1], - cmd, - ] - libtool = conf.get(ENV_LIBTOOL) - if libtool: - argv = list(libtool) + ["--mode=execute"] + argv - else: - argv = [cmd] - - argv.extend(args) - return argv, valgrind_log - - def call_pexpect(self, cmd_path, args, extra_env): - argv, valgrind_log = self.cmd_construct_argv(cmd_path, args) - env = self._env(extra_env=extra_env) - - pexp = pexpect.spawn(argv[0], argv[1:], timeout=10, env=env) - - typ = collections.namedtuple("CallPexpect", ["pexp", "valgrind_log"]) - return typ(pexp, valgrind_log) - def async_start(self, wait_all=False): while True: while True: - for async_job in list(self._async_jobs[0:MAX_JOBS]): + for async_job in list(self._async_jobs[0 : self.MAX_JOBS]): async_job.start() # start up to MAX_JOBS jobs, but poll() and complete those # that are already exited. Retry, until there are no more # jobs to start, or until MAX_JOBS are running. jobs_running = [] - for async_job in list(self._async_jobs[0:MAX_JOBS]): + for async_job in list(self._async_jobs[0 : self.MAX_JOBS]): if async_job.poll() is not None: self._async_jobs.remove(async_job) async_job.wait_and_complete() @@ -1031,7 +1095,7 @@ class TestNmClient(unittest.TestCase): jobs_running.append(async_job) if len(jobs_running) >= len(self._async_jobs): break - if len(jobs_running) >= MAX_JOBS: + if len(jobs_running) >= self.MAX_JOBS: break if not jobs_running: @@ -1051,7 +1115,10 @@ class TestNmClient(unittest.TestCase): def async_wait(self): return self.async_start(wait_all=True) - def _nm_test_post(self): + def async_append_job(self, async_job): + self._async_jobs.append(async_job) + + def run_post(self): self.async_wait() @@ -1059,8 +1126,8 @@ class TestNmClient(unittest.TestCase): self._calling_num = None - results = self._results - self._results = None + results = self.ctx_results + self.ctx_results = None if len(results) == 0: return @@ -1068,18 +1135,16 @@ class TestNmClient(unittest.TestCase): skip_test_for_l10n_diff = self._skip_test_for_l10n_diff self._skip_test_for_l10n_diff = None - test_name = self._testMethodName - filename = os.path.abspath( PathConfiguration.srcdir() + "/test-client.check-on-disk/" - + test_name + + self.testMethodName + ".expected" ) regenerate = conf.get(ENV_NM_TEST_REGENERATE) - content_expect, results_expect = self._read_expected(filename) + content_expect, results_expect = Util.file_read_expected(filename) if results_expect is None: if not regenerate: @@ -1156,21 +1221,21 @@ class TestNmClient(unittest.TestCase): # nmcli loads translations from the installation path. This failure commonly # happens because you did not install the binary in the --prefix, before # running the test. Hence, translations are not available or differ. - self.skipTest( + raise unittest.SkipTest( "Skipped asserting for localized tests %s. Set NM_TEST_CLIENT_CHECK_L10N=1 to force fail." % (",".join(skip_test_for_l10n_diff)) ) - def setUp(self): - if not dbus_session_inited: - self.skipTest( - "Own D-Bus session for testing is not initialized. Do you have dbus-run-session available?" - ) - if NM is None: - self.skipTest("gi.NM is not available. Did you build with introspection?") +############################################################################### + + +class TestNmcli(unittest.TestCase): + def setUp(self): + Util.skip_without_dbus_session() + Util.skip_without_NM() + self.ctx = NMTestContext(self._testMethodName) -class TestNmcli(TestNmClient): def call_nmcli_l( self, args, @@ -1253,9 +1318,6 @@ class TestNmcli(TestNmClient): frame, ) - def call_nmcli_pexpect(self, args): - return self.call_pexpect(ENV_NM_TEST_CLIENT_NMCLI_PATH, args, {"NO_COLOR": "1"}) - def _call_nmcli( self, args, @@ -1275,11 +1337,10 @@ class TestNmcli(TestNmClient): ): if sync_barrier: - self.async_wait() + self.ctx.async_wait() calling_fcn = frame.f_code.co_name - calling_num = self._calling_num.get(calling_fcn, 0) + 1 - self._calling_num[calling_fcn] = calling_num + calling_num = self.ctx.calling_num(calling_fcn) test_name = "%s-%03d" % (calling_fcn, calling_num) @@ -1316,7 +1377,7 @@ class TestNmcli(TestNmClient): self.fail("invalid language %s" % (lang)) # Running under valgrind is not yet supported for those tests. - args, valgrind_log = self.cmd_construct_argv( + args, valgrind_log = Util.cmd_create_argv( ENV_NM_TEST_CLIENT_NMCLI_PATH, args, with_valgrind=False ) @@ -1348,8 +1409,8 @@ class TestNmcli(TestNmClient): if expected_stderr is _DEFAULT_ARG: expected_stderr = None - results_idx = len(self._results) - self._results.append(None) + results_idx = len(self.ctx.ctx_results) + self.ctx.ctx_results.append(None) def complete_cb(async_job, returncode, stdout, stderr): @@ -1424,55 +1485,47 @@ class TestNmcli(TestNmClient): ) content = ("size: %s\n" % (len(content))).encode("utf8") + content - self._results[results_idx] = { + self.ctx.ctx_results[results_idx] = { "test_name": test_name, "ignore_l10n_diff": ignore_l10n_diff, "content": content, } - env = self._env(lang, calling_num, fatal_warnings, extra_env) + env = Util.cmd_create_env(lang, calling_num, fatal_warnings, extra_env) async_job = AsyncProcess(args=args, env=env, complete_cb=complete_cb) - self._async_jobs.append(async_job) + self.ctx.async_append_job(async_job) - self.async_start(wait_all=sync_barrier) + self.ctx.async_start(wait_all=sync_barrier) def nm_test(func): def f(self): - self.srv_start() + self.ctx.srv_start() func(self) - self._nm_test_post() + self.ctx.run_post() return f def nm_test_no_dbus(func): def f(self): func(self) - self._nm_test_post() - - return f - - def skip_without_pexpect(func): - def f(self): - if pexpect is None: - raise unittest.SkipTest("pexpect not available") - func(self) + self.ctx.run_post() return f def init_001(self): - self.srv.op_AddObj("WiredDevice", iface="eth0") - self.srv.op_AddObj("WiredDevice", iface="eth1") - self.srv.op_AddObj("WifiDevice", iface="wlan0") - self.srv.op_AddObj("WifiDevice", iface="wlan1") + self.ctx.srv.op_AddObj("WiredDevice", iface="eth0") + self.ctx.srv.op_AddObj("WiredDevice", iface="eth1") + self.ctx.srv.op_AddObj("WifiDevice", iface="wlan0") + self.ctx.srv.op_AddObj("WifiDevice", iface="wlan1") # add another device with an identical ifname. The D-Bus API itself # does not enforce the ifnames are unique. - self.srv.op_AddObj("WifiDevice", ident="wlan1/x", iface="wlan1") + self.ctx.srv.op_AddObj("WifiDevice", ident="wlan1/x", iface="wlan1") - self.srv.op_AddObj("WifiAp", device="wlan0", rsnf=0x0) + self.ctx.srv.op_AddObj("WifiAp", device="wlan0", rsnf=0x0) - self.srv.op_AddObj("WifiAp", device="wlan0") + self.ctx.srv.op_AddObj("WifiAp", device="wlan0") NM_AP_FLAGS = getattr(NM, "80211ApSecurityFlags") rsnf = 0x0 @@ -1481,11 +1534,11 @@ class TestNmcli(TestNmClient): rsnf = rsnf | NM_AP_FLAGS.GROUP_TKIP rsnf = rsnf | NM_AP_FLAGS.GROUP_CCMP rsnf = rsnf | NM_AP_FLAGS.KEY_MGMT_SAE - self.srv.op_AddObj("WifiAp", device="wlan0", wpaf=0x0, rsnf=rsnf) + self.ctx.srv.op_AddObj("WifiAp", device="wlan0", wpaf=0x0, rsnf=rsnf) - self.srv.op_AddObj("WifiAp", device="wlan1") + self.ctx.srv.op_AddObj("WifiAp", device="wlan1") - self.srv.addConnection( + self.ctx.srv.addConnection( {"connection": {"type": "802-3-ethernet", "id": "con-1"}} ) @@ -1545,7 +1598,9 @@ class TestNmcli(TestNmClient): replace_uuids = [] replace_uuids.append( - self.ReplaceTextConUuid("con-xx1", "UUID-con-xx1-REPLACED-REPLACED-REPLA") + self.ctx.srv.ReplaceTextConUuid( + "con-xx1", "UUID-con-xx1-REPLACED-REPLACED-REPLA" + ) ) self.call_nmcli( @@ -1558,7 +1613,7 @@ class TestNmcli(TestNmClient): for con_name, apn in con_gsm_list: replace_uuids.append( - self.ReplaceTextConUuid( + self.ctx.srv.ReplaceTextConUuid( con_name, "UUID-" + con_name + "-REPLACED-REPLACED-REPL" ) ) @@ -1590,7 +1645,9 @@ class TestNmcli(TestNmClient): ) replace_uuids.append( - self.ReplaceTextConUuid("ethernet", "UUID-ethernet-REPLACED-REPLACED-REPL") + self.ctx.srv.ReplaceTextConUuid( + "ethernet", "UUID-ethernet-REPLACED-REPLACED-REPL" + ) ) self.call_nmcli( @@ -1673,9 +1730,9 @@ class TestNmcli(TestNmClient): ["-f", "ALL", "-t", "dev", "show", "eth0"], replace_stdout=replace_uuids ) - self.async_wait() + self.ctx.async_wait() - self.srv.setProperty( + self.ctx.srv.setProperty( "/org/freedesktop/NetworkManager/ActiveConnection/1", "State", dbus.UInt32(NM.ActiveConnectionState.DEACTIVATING), @@ -1685,8 +1742,8 @@ class TestNmcli(TestNmClient): for i in [0, 1]: if i == 1: - self.async_wait() - self.srv.op_ConnectionSetVisible(False, con_id="ethernet") + self.ctx.async_wait() + self.ctx.srv.op_ConnectionSetVisible(False, con_id="ethernet") for mode in Util.iter_nmcli_output_modes(): self.call_nmcli_l( @@ -1719,7 +1776,9 @@ class TestNmcli(TestNmClient): replace_uuids = [] replace_uuids.append( - self.ReplaceTextConUuid("con-xx1", "UUID-con-xx1-REPLACED-REPLACED-REPLA") + self.ctx.srv.ReplaceTextConUuid( + "con-xx1", "UUID-con-xx1-REPLACED-REPLACED-REPLA" + ) ) self.call_nmcli( @@ -1762,10 +1821,12 @@ class TestNmcli(TestNmClient): ) self.call_nmcli_l(["con", "s", "con-xx1"], replace_stdout=replace_uuids) - self.async_wait() + self.ctx.async_wait() replace_uuids.append( - self.ReplaceTextConUuid("con-vpn-1", "UUID-con-vpn-1-REPLACED-REPLACED-REP") + self.ctx.srv.ReplaceTextConUuid( + "con-vpn-1", "UUID-con-vpn-1-REPLACED-REPLACED-REP" + ) ) self.call_nmcli( @@ -1796,16 +1857,16 @@ class TestNmcli(TestNmClient): self.call_nmcli_l(["con", "s"], replace_stdout=replace_uuids) self.call_nmcli_l(["con", "s", "con-vpn-1"], replace_stdout=replace_uuids) - self.async_wait() + self.ctx.async_wait() - self.srv.setProperty( + self.ctx.srv.setProperty( "/org/freedesktop/NetworkManager/ActiveConnection/2", "VpnState", dbus.UInt32(NM.VpnConnectionState.ACTIVATED), ) uuids = Util.replace_text_sort_list( - [c[1] for c in self.srv.findConnections()], replace_uuids + [c[1] for c in self.ctx.srv.findConnections()], replace_uuids ) self.call_nmcli_l([], replace_stdout=replace_uuids) @@ -2073,10 +2134,10 @@ class TestNmcli(TestNmClient): extra_env=no_dbus_env, ) - @skip_without_pexpect + @Util.skip_without_pexpect @nm_test def test_ask_mode(self): - nmc = self.call_nmcli_pexpect(["--ask", "c", "add"]) + nmc = Util.cmd_call_pexpect_nmcli(["--ask", "c", "add"]) nmc.pexp.expect("Connection type:") nmc.pexp.sendline("ethernet") nmc.pexp.expect("Interface name:") @@ -2097,11 +2158,11 @@ class TestNmcli(TestNmClient): nmc.pexp.expect(pexpect.EOF) Util.valgrind_check_log(nmc.valgrind_log, "test_ask_mode") - @skip_without_pexpect + @Util.skip_without_pexpect @nm_test def test_monitor(self): def start_mon(self): - nmc = self.call_nmcli_pexpect(["monitor"]) + nmc = Util.cmd_call_pexpect_nmcli(["monitor"]) nmc.pexp.expect("NetworkManager is running") return nmc @@ -2112,10 +2173,10 @@ class TestNmcli(TestNmClient): nmc = start_mon(self) - self.srv.op_AddObj("WiredDevice", iface="eth0") + self.ctx.srv.op_AddObj("WiredDevice", iface="eth0") nmc.pexp.expect("eth0: device created\r\n") - self.srv.addConnection( + self.ctx.srv.addConnection( {"connection": {"type": "802-3-ethernet", "id": "con-1"}} ) nmc.pexp.expect("con-1: connection profile created\r\n") @@ -2123,7 +2184,7 @@ class TestNmcli(TestNmClient): end_mon(self, nmc) nmc = start_mon(self) - self.srv_shutdown() + self.ctx.srv_shutdown() Util.pexpect_expect_all( nmc.pexp, "con-1: connection profile removed", @@ -2136,15 +2197,25 @@ class TestNmcli(TestNmClient): ############################################################################### -class TestNmCloudSetup(TestNmClient): +class TestNmCloudSetup(unittest.TestCase): + def setUp(self): + Util.skip_without_dbus_session() + Util.skip_without_NM() + self.ctx = NMTestContext(self._testMethodName) + + _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. """ def f(self): - if pexpect is None: - raise unittest.SkipTest("pexpect not available") + Util.skip_without_pexpect() if tuple(sys.version_info[0:2]) < (3, 2): # subprocess.Popen()'s "pass_fd" argument requires at least Python 3.2. @@ -2160,35 +2231,48 @@ 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], + [sys.executable, service_path, "--empty"], stdin=subprocess.PIPE, env=env, - pass_fds=(s.fileno(),), + 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() - self.srv_start() - func(self) - self._nm_test_post() + error = None + + self.ctx.srv_start() + try: + func(self) + except Exception as e: + error = e + self.ctx.run_post() + self.md_conn.close() p.stdin.close() p.terminate() p.wait() - return f + if error: + raise error - @cloud_setup_test - def test_ec2(self): + return f + def _mock_devices(self): # Add a device with an active connection that has IPv4 configured - self.srv.op_AddObj("WiredDevice", iface="eth0") - self.srv.addAndActivateConnection( + self.ctx.srv.op_AddObj("WiredDevice", iface="eth0", mac="9e:c0:3e:92:24:2d") + self.ctx.srv.addAndActivateConnection( { "connection": {"type": "802-3-ethernet", "id": "con-eth0"}, "ipv4": {"method": "auto"}, @@ -2198,16 +2282,220 @@ class TestNmCloudSetup(TestNmClient): ) # The second connection has no IPv4 - self.srv.op_AddObj("WiredDevice", iface="eth1") - self.srv.addAndActivateConnection( + self.ctx.srv.op_AddObj("WiredDevice", iface="eth1", mac="53:e9:7e:52:8d:a8") + self.ctx.srv.addAndActivateConnection( {"connection": {"type": "802-3-ethernet", "id": "con-eth1"}}, "/org/freedesktop/NetworkManager/Devices/2", "", 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_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( + nmc = Util.cmd_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 = Util.cmd_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_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 = Util.cmd_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 = Util.cmd_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() + + _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 = Util.cmd_call_pexpect( ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, [], { @@ -2229,7 +2517,7 @@ class TestNmCloudSetup(TestNmClient): nmc.pexp.expect(pexpect.EOF) # Run nm-cloud-setup for the second time - nmc = self.call_pexpect( + nmc = Util.cmd_call_pexpect( ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH, [], { @@ -2251,6 +2539,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 = Util.cmd_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 = Util.cmd_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") + ############################################################################### diff --git a/tools/test-cloud-meta-mock.py b/tools/test-cloud-meta-mock.py index 392955b8ad..ab3630addf 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,36 +30,40 @@ 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,16 +75,89 @@ 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/" + + 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" + + 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" + + 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, + 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", + 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", + 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, + } +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_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() @@ -78,8 +165,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() |