diff --git a/Makefile.am b/Makefile.am index 2a4b961..daefd26 100644 --- a/Makefile.am +++ b/Makefile.am @@ -16,6 +16,7 @@ EXTRA_DIST = \ LICENSE \ COPYING \ data/ucd.service.in \ + data/ucd@.service.in \ docs/ucd.1.md \ docs/ucd-data-fetch.1.md \ docs/cloud-config.5.md @@ -87,10 +88,7 @@ ucd_data_fetch_CFLAGS = $(AM_CFLAGS) SYSTEMD_DIR=$(prefix)/lib/systemd/system/ systemdsystemunitdir = @SYSTEMD_SYSTEMUNITDIR@ systemdsystemunit_DATA = data/ucd.service \ - data/ucd-aws.service \ - data/ucd-oci.service \ - data/ucd-tencent.service \ - data/ucd-aliyun.service + data/ucd@.service systemdsystemunit-install-local: mkdir -p $(DESTDIR)$(systemdsystemunitdir)/multi-user.target.wants/ diff --git a/configure.ac b/configure.ac index 8542e81..c2c7063 100644 --- a/configure.ac +++ b/configure.ac @@ -8,10 +8,7 @@ AC_CONFIG_SRCDIR([src/main.c]) AC_CONFIG_FILES([Makefile tests/Makefile data/ucd.service - data/ucd-aws.service - data/ucd-oci.service - data/ucd-tencent.service - data/ucd-aliyun.service]) + data/ucd@.service]) AC_CONFIG_HEADERS([config.h]) LT_INIT diff --git a/data/ucd-aliyun.service.in b/data/ucd-aliyun.service.in deleted file mode 100644 index 33c4264..0000000 --- a/data/ucd-aliyun.service.in +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=micro-config-drive job for Aliyun -After=network.target systemd-networkd.service -Wants=local-fs.target sshd.service sshd-keygen.service -ConditionPathExists=!/var/lib/cloud/aliyun-user-data - -[Service] -Type=oneshot -ExecStart=@prefix@/bin/ucd-data-fetch aliyun -RemainAfterExit=yes -TimeoutSec=0 - -# Output needs to appear in instance console output -StandardOutput=journal+console - -[Install] -WantedBy=multi-user.target diff --git a/data/ucd-oci.service.in b/data/ucd-oci.service.in deleted file mode 100644 index cddb7b0..0000000 --- a/data/ucd-oci.service.in +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=micro-config-drive job for OCI -After=network.target systemd-networkd.service -Wants=local-fs.target sshd.service sshd-keygen.service -ConditionPathExists=!/var/lib/cloud/oci-user-data - -[Service] -Type=oneshot -ExecStart=@prefix@/bin/ucd-data-fetch oci -RemainAfterExit=yes -TimeoutSec=0 - -# Output needs to appear in instance console output -StandardOutput=journal+console - -[Install] -WantedBy=multi-user.target diff --git a/data/ucd-tencent.service.in b/data/ucd-tencent.service.in deleted file mode 100644 index 776fb3d..0000000 --- a/data/ucd-tencent.service.in +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=micro-config-drive job for TENCENT -After=network.target systemd-networkd.service -Wants=local-fs.target sshd.service sshd-keygen.service -ConditionPathExists=!/var/lib/cloud/tencent-user-data - -[Service] -Type=oneshot -ExecStart=@prefix@/bin/ucd-data-fetch tencent -RemainAfterExit=yes -TimeoutSec=0 - -# Output needs to appear in instance console output -StandardOutput=journal+console - -[Install] -WantedBy=multi-user.target diff --git a/data/ucd-aws.service.in b/data/ucd@.service.in similarity index 68% rename from data/ucd-aws.service.in rename to data/ucd@.service.in index aaf4f4b..a4620a6 100644 --- a/data/ucd-aws.service.in +++ b/data/ucd@.service.in @@ -1,12 +1,12 @@ [Unit] -Description=micro-config-drive job for AWS +Description=micro-config-drive job for %I After=network.target systemd-networkd.service Wants=local-fs.target sshd.service sshd-keygen.service -ConditionPathExists=!/var/lib/cloud/aws-user-data +ConditionPathExists=!/var/lib/cloud/%I-user-data [Service] Type=oneshot -ExecStart=@prefix@/bin/ucd-data-fetch aws +ExecStart=@prefix@/bin/ucd-data-fetch %I RemainAfterExit=yes TimeoutSec=0 diff --git a/src/ucd-data-fetch.c b/src/ucd-data-fetch.c index c0da102..6736dbc 100644 --- a/src/ucd-data-fetch.c +++ b/src/ucd-data-fetch.c @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -60,28 +61,30 @@ struct cloud_struct { char *name; char *ip; + uint16_t port; char *request_sshkey_path; char *request_userdata_path; char *cloud_config_header; }; -#define MAX_CONFIGS 4 +#define MAX_CONFIGS 6 static struct cloud_struct config[MAX_CONFIGS] = { { "aws", "169.254.169.254", + 80, "/latest/meta-data/public-keys/0/openssh-key", "/latest/user-data", "#cloud-config\n" \ "users:\n" \ " - name: clear\n" \ " groups: wheelnopw\n" \ - "ssh_authorized_keys:\n" \ - " - " + "ssh_authorized_keys:\n" }, { "oci", "169.254.169.254", + 80, "/opc/v1/instance/metadata/ssh_authorized_keys", NULL, "#cloud-config\n" \ @@ -89,32 +92,55 @@ static struct cloud_struct config[MAX_CONFIGS] = { " - name: opc\n" \ " groups: wheelnopw\n" \ " gecos: Oracle Public Cloud User\n" \ - "ssh_authorized_keys:\n" \ - " - " + "ssh_authorized_keys:\n" }, { "tencent", "169.254.0.23", + 80, "/latest/meta-data/public-keys/0/openssh-key", NULL, "#cloud-config\n" \ "users:\n" \ " - name: tencent\n" \ " groups: wheelnopw\n" \ - "ssh_authorized_keys:\n" \ - " - " + "ssh_authorized_keys:\n" }, { "aliyun", "100.100.100.200", + 80, "/latest/meta-data/public-keys/0/openssh-key", NULL, "#cloud-config\n" \ "users:\n" \ " - name: aliyun\n" \ " groups: wheelnopw\n" \ - "ssh_authorized_keys:\n" \ - " - " + "ssh_authorized_keys:\n" + }, + { + "equinix", + "metadata.platformequinix.com", + 80, + "/2009-04-04/meta-data/public-keys", + "/userdata", + "#cloud-config\n" \ + "users:\n" \ + " - name: clear\n" \ + " groups: wheelnopw\n" \ + "ssh_authorized_keys:\n" + }, + { + "test", + "127.0.0.254", + 8123, + "/public-keys", + "/user-data", + "#cloud-config\n" \ + "users:\n" \ + " - name: clear\n" \ + " groups: wheelnopw\n" \ + "ssh_authorized_keys:\n" } }; @@ -162,31 +188,36 @@ static int parse_headers(FILE *f, size_t *cl) } /** - * write_lines() - write remaining data from stream f into out, while minding cl length + * write_lines() - write remaining lines from stream f into out, while minding cl length + * - if prefix != NULL, each line written is prefixed with the prefix. * - returns 0 on success, 1 on failure - * - after this call, the calue of `cl` outside the function is invalid. + * - after this call, the value of `cl` outside the function is invalid. */ -static int write_lines(int out, FILE *f, size_t cl) +static int write_lines(int out, FILE *f, size_t cl, const char *prefix) { for (;;) { if (cl == 0) { return 0; } - size_t len; - char buf[512] = {0}; + char buf[2048] = {0}; - len = (cl > sizeof(buf)) ? sizeof(buf) : cl; - - size_t r = fread(buf, 1, len, f); + char *r = fgets(buf, sizeof(buf), f); if (ferror(f)) { return 1; - } else if (r == 0) { + } else if (!r) { return 0; } - cl -= r; - if (write(out, buf, r) < (ssize_t)r) { + size_t len = strlen(r); + cl -= len; + + if (prefix) { + if (write(out, prefix, strlen(prefix)) < (ssize_t)strlen(prefix)) + return 1; + } + + if (write(out, buf, len) < (ssize_t)len) { return 1; } } @@ -233,12 +264,42 @@ int main(int argc, char *argv[]) { memset(&server, 0, sizeof(struct sockaddr_in)); server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr(config[conf].ip); - server.sin_port = htons(80); + server.sin_port = htons(config[conf].port); struct timespec ts; ts.tv_sec = 0; ts.tv_nsec = 50000000; + /* Do we need to look up a hostname? */ + if ((int) server.sin_addr.s_addr == -1) { + n = 0; + for (;;) { + struct hostent *hp = gethostbyname(config[conf].ip); + if (hp != NULL) { + if (hp->h_length > 0) { + /* Got it; use the resulting IP address */ + server.sin_family = (short unsigned int) (hp->h_addrtype & 0xFFFF); + memcpy(&(server.sin_addr.s_addr), hp->h_addr, (size_t) hp->h_length); + break; + } + else { + fprintf(stderr, "gethostbyname(): empty response"); + exit(EXIT_FAILURE); + } + } + + if ((h_errno != TRY_AGAIN) && (h_errno != NO_RECOVERY)) { + herror("gethostbyname()"); + exit(EXIT_FAILURE); + } + nanosleep(&ts, NULL); + if (++n > 2000) { /* 100 secs */ + herror("gethostbyname()"); + exit(EXIT_FAILURE); + } + } + } + for (;;) { int r = connect(sockfd, (struct sockaddr *)&server, sizeof(server)); if (r == 0) { @@ -248,7 +309,7 @@ int main(int argc, char *argv[]) { FAIL("connect()"); } nanosleep(&ts, NULL); - if (++n > 200) { /* 10 secs */ + if (++n > 2400) { /* 120 secs - any used up in gethostbyname */ FAIL("timeout in connect()"); } } @@ -275,36 +336,56 @@ int main(int argc, char *argv[]) { size_t cl; int result = parse_headers(f, &cl); if (result != 1) { - close(sockfd); + fclose(f); FAIL("parse_headers()"); } - close(sockfd); - int out; (void) mkdir(USER_DATA_PATH, 0); if (asprintf(&outpath, "%s/%s-user-data", USER_DATA_PATH, config[conf].name) < 0) { + fclose(f); FAIL("asprintf()"); } + /* Special case for testing -- can't use/don't need privileged directory */ + if (0 == strcmp(config[conf].name, "test")) { + if (asprintf(&outpath, "%s-user-data", config[conf].name) < 0) { + fclose(f); + FAIL("asprintf()"); + } + } (void) unlink(outpath); out = open(outpath, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); if (out < 0) { + fclose(f); FAIL("open()"); } /* Insert cloud-config header above SSH key. */ len = strlen(config[conf].cloud_config_header); if (write(out, config[conf].cloud_config_header, len) < (ssize_t)len) { + close(out); + fclose(f); + unlink(outpath); FAIL("write()"); } - if (write_lines(out, f, cl) != 0) { + /* Write out SSH keys */ + if (write_lines(out, f, cl, " - ") != 0) { close(out); fclose(f); unlink(outpath); FAIL("write_lines()"); } + /* Write an extra linefeed in case this didn't end with one */ + if (write(out, "\n", 1) < (ssize_t) 1) { + close(out); + fclose(f); + unlink(outpath); + FAIL("write()"); + } + close(sockfd); + /* reopen socket */ sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { @@ -357,7 +438,7 @@ int main(int argc, char *argv[]) { } /* don't write part #2 if 404 or some non-error */ - if ((result != 2) && (write_lines(out, f, cl) != 0)) { + if ((result != 2) && (write_lines(out, f, cl, NULL) != 0)) { close(out); fclose(f); unlink(outpath); @@ -370,6 +451,11 @@ int main(int argc, char *argv[]) { finish: - (void) execl(BINDIR "/ucd", BINDIR "/ucd", "-u", outpath, (char *)NULL); - FAIL("exec()"); + /* Don't run ucd for the test template */ + if (strcmp(config[conf].name, "test") != 0) { + (void) execl(BINDIR "/ucd", BINDIR "/ucd", "-u", outpath, (char *)NULL); + FAIL("exec()"); + } + + return 0; } diff --git a/tests/Makefile.am b/tests/Makefile.am index a49c8c9..6e25ac9 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -6,13 +6,16 @@ ACLOCAL_AMFLAGS = -I m4 TESTS = +EXTRA_DIST = + check_PROGRAMS = +check_SCRIPTS = check_LTLIBRARIES = libtest.la COMMON_CFLAGS = -std=gnu99 -I$(top_srcdir)/src -I$(top_srcdir)/src/ccmodules \ -I$(top_srcdir)/src/interpreters \ - $(CHECK_FLAGS) $(GLIB_CFLAGS) $(CURL_CFLAGS) $(YAML_CFLAGS) $(BLKID_CFLAGS) $(PARTED_CFLAGS) -COMMON_LDADD = $(CHECK_LIBS) $(GLIB_LIBS) $(CURL_LIBS) $(YAML_LIBS) $(BLKID_LIBS) $(PARTED_LIBS) + $(CHECK_FLAGS) $(GLIB_CFLAGS) $(YAML_CFLAGS) $(BLKID_CFLAGS) $(PARTED_CFLAGS) +COMMON_LDADD = $(CHECK_LIBS) $(GLIB_LIBS) $(YAML_LIBS) $(BLKID_LIBS) $(PARTED_LIBS) libtest_la_SOURCES = \ ../src/lib.c \ @@ -45,14 +48,18 @@ lib_test_SOURCES = lib_test.c lib_test_CFLAGS = $(COMMON_CFLAGS) $(AM_CFLAGS) lib_test_LDADD = libtest.la $(COMMON_LDADD) TESTS += lib_test +check_PROGRAMS += lib_test userdata_test_SOURCES = userdata_test.c userdata_test_CFLAGS = $(COMMON_CFLAGS) $(AM_CFLAGS) userdata_test_LDADD = libtest.la $(COMMON_LDADD) TESTS += userdata_test +check_PROGRAMS += userdata_test - -check_PROGRAMS += $(TESTS) +# fetch_test is a shell script +TESTS += fetch_test +check_SCRIPTS += fetch_test +EXTRA_DIST += fetch_test fetch_data CLEANFILES = *~ *.log diff --git a/tests/fetch_data/expected b/tests/fetch_data/expected new file mode 100644 index 0000000..7b6b799 --- /dev/null +++ b/tests/fetch_data/expected @@ -0,0 +1,17 @@ +#cloud-config +users: + - name: clear + groups: wheelnopw +ssh_authorized_keys: + - SSH_TEST_KEY_STRING_1 + - SSH_TEST_KEY_STRING_2 + - SSH_TEST_KEY_STRING_3 + +#cloud-config + +users: + - name: test1 + groups: group1 + - name: test2 + groups: group2 + diff --git a/tests/fetch_data/public-keys b/tests/fetch_data/public-keys new file mode 100644 index 0000000..b6f8e61 --- /dev/null +++ b/tests/fetch_data/public-keys @@ -0,0 +1,3 @@ +SSH_TEST_KEY_STRING_1 +SSH_TEST_KEY_STRING_2 +SSH_TEST_KEY_STRING_3 diff --git a/tests/fetch_data/user-data b/tests/fetch_data/user-data new file mode 100644 index 0000000..7f7fb10 --- /dev/null +++ b/tests/fetch_data/user-data @@ -0,0 +1,8 @@ +#cloud-config + +users: + - name: test1 + groups: group1 + - name: test2 + groups: group2 + diff --git a/tests/fetch_test b/tests/fetch_test new file mode 100755 index 0000000..f0a9cff --- /dev/null +++ b/tests/fetch_test @@ -0,0 +1,23 @@ +#!/bin/bash -x + +set -euo pipefail +SCRIPT_PATH="$(dirname "$(readlink -f "${BASH_SOURCE}")")" + +# Launch a lightweight HTTP server and attempt to fetch cloud config from it +# Uses the "test" template in ucd-fetch-data + +cd "${SCRIPT_PATH}/fetch_data" +python -m http.server 8123 --bind 127.0.0.254 & +HTTP_PID=$! +trap "sleep 1; kill ${HTTP_PID}" EXIT +cd "${SCRIPT_PATH}" + +sleep 2 + +../ucd-data-fetch test + +# Compare what we got/generated with what we expect +cmp -b fetch_data/expected test-user-data + +# Cleanup the test data file +rm test-user-data