diff options
Diffstat (limited to 'tools/test-cloud-meta-mock.py')
| -rwxr-xr-x | tools/test-cloud-meta-mock.py | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/tools/test-cloud-meta-mock.py b/tools/test-cloud-meta-mock.py new file mode 100755 index 00000000..bc0a829e --- /dev/null +++ b/tools/test-cloud-meta-mock.py @@ -0,0 +1,285 @@ +#!/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 +import sys + +from http.server import HTTPServer +from http.server import BaseHTTPRequestHandler +from socketserver import BaseServer + + +PROVIDERS = [ + "aliyun", + "azure", + "ec2", + "gcp", +] + + +def _s_to_bool(s): + s0 = s + if isinstance(s, bytes): + s = s.encode("utf-8", errors="replace") + if isinstance(s, str): + s = s.lower() + if s in ["yes", "y", "true", "1"]: + return True + if s in ["no", "n", "false", "0"]: + return False + if isinstance(s, int): + if s in [0, 1]: + return s == 1 + raise ValueError('Not a boolean value ("%s")' % (s0,)) + + +DEBUG = _s_to_bool(os.environ.get("NM_TEST_CLOUD_SETUP_MOCK_DEBUG", "0")) + + +def dbg(msg): + if DEBUG: + print("DBG: %s" % (msg,)) + + +class MockCloudMDRequestHandler(BaseHTTPRequestHandler): + """ + Respond to cloud metadata service requests. + Currently implements a fairly minimal subset of AWS EC2 API. + """ + + def log_message(self, format, *args): + pass + + def _response_and_end(self, code, write=None): + self.send_response(code) + self.end_headers() + if write is None: + dbg("response %s" % (code,)) + else: + if isinstance(write, str): + write = write.encode("utf-8") + dbg("response %s, %s" % (code, write)) + self.wfile.write(write) + + def _read(self): + length = int(self.headers["content-length"]) + v = self.rfile.read(length) + dbg('receive "%s"' % (v,)) + return v + + def do_GET(self): + path = self.path.encode("ascii") + dbg("GET %s" % (path,)) + r = None + if path in self.server._resources: + r = self.server._resources[path] + elif self.server.config_get_allow_default(): + for p in self.server.config_get_providers(): + if path in DEFAULT_RESOURCES[p]: + r = DEFAULT_RESOURCES[p][path] + break + if r is None: + self._response_and_end(404) + return + self._response_and_end(200, write=r) + + def do_PUT(self): + path = self.path.encode("ascii") + dbg("PUT %s" % (path,)) + if path.startswith(b"/.nmtest/"): + conf_name = path[len(b"/.nmtest/") :] + v = self._read() + + self.server._config[conf_name] = v + + assert self.server.config_get_providers() is not None + assert self.server.config_get_allow_default() is not None + + self._response_and_end(201) + elif path == b"/latest/api/token": + if "ec2" not in self.server.config_get_providers(): + self._response_and_end(404) + else: + self._response_and_end( + 200, + write="AQAAALH-k7i18JMkK-ORLZQfAa7nkNjQbKwpQPExNHqzk1oL_7eh-A==", + ) + else: + self.server._resources[path] = self._read() + self._response_and_end(201) + + def do_DELETE(self): + path = self.path.encode("ascii") + dbg("DELETE %s" % (path,)) + if path in self.server._resources: + del self.server._resources[path] + self._response_and_end(204) + else: + self._response_and_end(404) + + +class SocketHTTPServer(HTTPServer): + """ + A HTTP server that accepts a socket (that has already been + listen()-ed on). This is useful when the socket is passed + fron the test runner. + """ + + def __init__( + self, + server_address, + RequestHandlerClass, + socket, + resources=None, + allow_default=True, + ): + BaseServer.__init__(self, server_address, RequestHandlerClass) + self.socket = socket + self.server_address = self.socket.getsockname() + self._resources = resources or {} + self._config = { + "allow-default": "yes" if allow_default else "no", + } + + def config_get_providers(self): + conf = self._config.get(b"providers", None) + if not conf: + return PROVIDERS + parsed = [s.lower() for s in conf.decode("utf-8", errors="replace").split(" ")] + assert all(p in PROVIDERS for p in parsed) + return parsed + + def config_get_allow_default(self): + return _s_to_bool(self._config.get(b"allow-default", "yes")) + + +def create_default_resources_for_provider(provider): + + mac1 = b"cc:00:00:00:00:01" + mac2 = b"cc:00:00:00:00:02" + + ip1 = b"172.31.26.249" + ip2 = b"172.31.176.249" + + if provider == "aliyun": + aliyun_meta = b"/2016-01-01/meta-data/" + aliyun_macs = aliyun_meta + b"network/interfaces/macs/" + return { + 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", + } + + if provider == "azure": + azure_meta = b"/metadata/instance" + azure_iface = azure_meta + b"/network/interface/" + azure_query = b"?format=text&api-version=2017-04-02" + return { + 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", + } + + if provider == "ec2": + ec2_macs = b"/2018-09-24/meta-data/network/interfaces/macs/" + 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, + }, + ) + + if provider == "gcp": + gcp_meta = b"/computeMetadata/v1/instance/" + gcp_iface = gcp_meta + b"network-interfaces/" + return { + 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, + } + + raise ValueError("invalid provider %s" % (provider,)) + + +def create_default_resources(): + + return {p: create_default_resources_for_provider(p) for p in PROVIDERS} + + +DEFAULT_RESOURCES = create_default_resources() + + +allow_default = True +try: + if sys.argv[1] == "--empty": + allow_default = False +except IndexError: + pass + +# See sd_listen_fds(3) +fileno = os.getenv("LISTEN_FDS") +if fileno is not None: + if fileno != "1": + raise Exception("Bad LISTEN_FDS") + s = socket.socket(fileno=3) +else: + addr = ("localhost", 0) + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + s.bind(addr) + +httpd = SocketHTTPServer( + None, + MockCloudMDRequestHandler, + socket=s, + allow_default=allow_default, +) + +print("Listening on http://%s:%d" % (httpd.server_address[0], httpd.server_address[1])) +httpd.server_activate() + +httpd.serve_forever() +httpd.server_close() |