From d9899d488a4bc209f57d296f1045ba00650c926d Mon Sep 17 00:00:00 2001 From: Reyk Floeter Date: Mon, 13 Aug 2018 19:30:41 +0200 Subject: [PATCH] Add initial support for OpenNebula contextualization. Thanks to datacenterlight.ch by ungleich glarus AG for providing access to their OpenNebula-based cloud. --- README.md | 3 + agent/Makefile | 7 +- agent/azure.c | 27 ++---- agent/cloudinit.c | 2 + agent/main.c | 188 +++++++++++++++++++++++++++++++++++++++- agent/main.h | 36 ++++++++ agent/opennebula.c | 209 +++++++++++++++++++++++++++++++++++++++++++++ agent/openstack.c | 2 + 8 files changed, 449 insertions(+), 25 deletions(-) create mode 100644 agent/opennebula.c diff --git a/README.md b/README.md index fea3ef0..8de4435 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ Installation is easy, `cloud-agent` detects the cloud type automatically. * On OpenStack/VMware, create a file `/etc/hostname.vmx0` +* On OpenNebula, create a file `/etc/hostname.if` + where _if_ is the name of your primary interface. + * The content of the file is identical for all of them: dhcp diff --git a/agent/Makefile b/agent/Makefile index 2205fc1..8d57acb 100644 --- a/agent/Makefile +++ b/agent/Makefile @@ -1,5 +1,6 @@ PROG= cloud-agent -SRCS= azure.c cloudinit.c http.c json.c jsmn.c log.c openstack.c main.c xml.c +SRCS= http.c json.c jsmn.c log.c main.c xml.c +SRCS+= azure.c cloudinit.c opennebula.c openstack.c BINDIR= /usr/local/libexec MANDIR= /usr/local/man/man @@ -15,7 +16,7 @@ CFLAGS+= -Wmissing-declarations CFLAGS+= -Wshadow -Wpointer-arith CFLAGS+= -Wsign-compare -Wcast-qual -LDADD+= -lexpat -ltls -lssl -lcrypto -DPADD+= ${LIBEXPAT} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO} +LDADD+= -lexpat -ltls -lssl -lcrypto -lutil +DPADD+= ${LIBEXPAT} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO} ${LIBUTIL} .include diff --git a/agent/azure.c b/agent/azure.c index d4a0b4a..40efd03 100644 --- a/agent/azure.c +++ b/agent/azure.c @@ -62,13 +62,14 @@ azure(struct system_config *sc) { int ret = -1; + sc->sc_stack = "azure"; + /* Apply defaults */ free(sc->sc_username); if ((sc->sc_username = strdup("azure-user")) == NULL) { log_warnx("failed to set default user"); goto fail; } - sc->sc_cdrom = "/dev/cd0c"; sc->sc_ovfenv = "/var/db/azure-ovf-env.xml"; sc->sc_priv = &az_config; @@ -654,36 +655,22 @@ azure_getovfenv(struct system_config *sc) struct xml xml; struct xmlelem *xp, *xe, *xk, *xv; char *sshfp, *sshval, *str; - int mount = 0, ret = -1, fd = -1; + int ret = -1, fd = -1; FILE *fp; - /* Silently try to mount the cdrom */ - fd = disable_output(sc, STDERR_FILENO); - ret = shell("mount", "-r", sc->sc_cdrom, "/mnt", NULL); - enable_output(sc, STDERR_FILENO, fd); - fd = -1; - - if (ret == 0) { - log_debug("%s: mounted %s", __func__, sc->sc_cdrom); - mount = 1; - } - ret = -1; - if (xml_init(&xml) != 0) { log_debug("%s: xml", __func__); goto done; } - /* Fallback to and older ovf-env.xml file */ + /* + * Assume that the cdrom is already mounted. + * Fallback to and older ovf-env.xml file. + */ if (xml_parse(&xml, "/mnt/ovf-env.xml") == -1 && xml_parse(&xml, sc->sc_ovfenv) == -1) goto done; - /* unmount if we mounted the cdrom before */ - if (mount && shell("umount", "/mnt", NULL) == 0) { - log_debug("%s: unmounted %s", __func__, sc->sc_cdrom); - } - if ((xp = xml_findl(&xml.ox_root, "Environment", "wa:ProvisioningSection", "LinuxProvisioningConfigurationSet", NULL)) == NULL) { diff --git a/agent/cloudinit.c b/agent/cloudinit.c index c6bf38c..807d784 100644 --- a/agent/cloudinit.c +++ b/agent/cloudinit.c @@ -41,6 +41,7 @@ ec2(struct system_config *sc) return (-1); } + sc->sc_stack = "ec2"; return (cloudinit_fetch(sc)); } @@ -53,6 +54,7 @@ cloudinit(struct system_config *sc) return (-1); } + sc->sc_stack = "cloudinit"; return (cloudinit_fetch(sc)); } diff --git a/agent/main.c b/agent/main.c index b01820b..706d3d8 100644 --- a/agent/main.c +++ b/agent/main.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Reyk Floeter + * Copyright (c) 2017, 2018 Reyk Floeter * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -15,19 +15,24 @@ */ #include +#include #include #include +#include #include +#include #include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -40,6 +45,7 @@ __dead void usage(void); static struct system_config *agent_init(const char *, int, int); static int agent_configure(struct system_config *); +static int agent_network(struct system_config *); static void agent_free(struct system_config *); static int agent_pf(struct system_config *, int); static int agent_userdata(const unsigned char *, size_t); @@ -303,14 +309,17 @@ static struct system_config * agent_init(const char *ifname, int dryrun, int timeout) { struct system_config *sc; + int fd, ret; if ((sc = calloc(1, sizeof(*sc))) == NULL) return (NULL); sc->sc_interface = ifname; + sc->sc_cdrom = "/dev/cd0c"; sc->sc_dryrun = dryrun ? 1 : 0; sc->sc_timeout = agent_timeout = timeout < 1 ? -1 : timeout * 1000; TAILQ_INIT(&sc->sc_pubkeys); + TAILQ_INIT(&sc->sc_netaddrs); if ((sc->sc_nullfd = open("/dev/null", O_RDWR)) == -1) { free(sc); @@ -322,6 +331,15 @@ agent_init(const char *ifname, int dryrun, int timeout) return (NULL); } + /* Silently try to mount the cdrom */ + fd = disable_output(sc, STDERR_FILENO); + ret = shell("mount", "-r", sc->sc_cdrom, "/mnt", NULL); + enable_output(sc, STDERR_FILENO, fd); + if (ret == 0) { + log_debug("%s: mounted %s", __func__, sc->sc_cdrom); + sc->sc_mount = 1; + } + if (sc->sc_dryrun) return (sc); @@ -337,6 +355,12 @@ static void agent_free(struct system_config *sc) { struct ssh_pubkey *ssh; + struct net_addr *net; + + /* unmount if we mounted the cdrom before */ + if (sc->sc_mount && shell("umount", "/mnt", NULL) == 0) { + log_debug("%s: unmounted %s", __func__, sc->sc_cdrom); + } free(sc->sc_hostname); free(sc->sc_username); @@ -352,6 +376,12 @@ agent_free(struct system_config *sc) TAILQ_REMOVE(&sc->sc_pubkeys, ssh, ssh_entry); free(ssh); } + + while ((net = TAILQ_FIRST(&sc->sc_netaddrs))) { + TAILQ_REMOVE(&sc->sc_netaddrs, net, net_entry); + free(net->net_value); + free(net); + } } int @@ -405,6 +435,103 @@ agent_setpubkey(struct system_config *sc, const char *sshval, const char *sshfp) return (ret); } +struct net_addr * +agent_getnetaddr(struct system_config *sc, struct net_addr *net) +{ + struct net_addr *na; + + TAILQ_FOREACH(na, &sc->sc_netaddrs, net_entry) { + if (na->net_type != net->net_type) + continue; + if (na->net_ifunit != net->net_ifunit) + continue; + if (net->net_addr.ss_family != AF_UNSPEC) { + if (na->net_addr.ss_family != + net->net_addr.ss_family) + continue; + if (memcmp(&na->net_addr, &net->net_addr, + na->net_addr.ss_len) != 0) + continue; + } + + return (na); + } + + return (NULL); +} + +int +agent_addnetaddr(struct system_config *sc, unsigned int unit, + const char *value, int af, enum net_type type) +{ + const char *errstr; + struct addrinfo hints, *res; + struct net_addr *net, *na; + + if ((net = calloc(1, sizeof(*net))) == NULL) { + log_debug("%s: calloc", __func__); + return (-1); + } + net->net_ifunit = unit; + net->net_type = type; + + switch (type) { + case NET_MAC: + if (ether_aton(value) == NULL) { + log_debug("%s: if%u mac %s", __func__, unit, value); + free(net); + return (-1); + } + break; + case NET_MTU: + case NET_PREFIX: + net->net_num = strtonum(value, 0, UINT32_MAX, &errstr); + if (errstr != NULL) { + log_debug("%s: if%u %s", __func__, unit, value); + free(net); + return (-1); + } + break; + default: + memset(&hints, 0, sizeof(hints)); + hints.ai_family = af; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_NUMERICHOST; + if (getaddrinfo(value, "0", &hints, &res) != 0) { + log_debug("%s: invalid address %s", + __func__, value); + free(net); + return (-1); + } + + if (res->ai_addrlen > sizeof(net->net_addr)) { + log_debug("%s: address too long", + __func__); + free(net); + freeaddrinfo(res); + return (-1); + } + memcpy(&net->net_addr, res->ai_addr, res->ai_addrlen); + net->net_addr.ss_len = res->ai_addrlen; + net->net_addr.ss_family = res->ai_family; + } + + /* Address already exists, ignore new entry */ + if ((na = agent_getnetaddr(sc, net)) != NULL) { + free(net); + return (0); + } + + if ((net->net_value = strdup(value)) == NULL) { + free(net); + return (-1); + } + + TAILQ_INSERT_TAIL(&sc->sc_netaddrs, net, net_entry); + + return (0); +} + static int fileout(const char *str, const char *mode, const char *fmt, ...) { @@ -575,6 +702,9 @@ agent_configure(struct system_config *sc) log_warnx("user-data failed"); } + if (agent_network(sc) != 0) + log_warnx("network configuration failed"); + log_debug("%s: %s", __func__, "/etc/rc.firsttime"); if (fileout("logger -s -t cloud-agent <sc_network) + return (0); + + if (strlcpy(ift, sc->sc_interface, sizeof(ift)) >= sizeof(ift)) + return (-1); + ift[strcspn(ift, "0123456789")] = '\0'; + + TAILQ_FOREACH(net, &sc->sc_netaddrs, net_entry) { + snprintf(ifname, sizeof(ifname), "%s%u", ift, net->net_ifunit); + switch (net->net_type) { + case NET_IP: + family = net->net_addr.ss_family == AF_INET ? + "inet" : "inet6"; + /* XXX prefix */ + + /* hostname.if startup configuration */ + snprintf(line, sizeof(line), "%s alias %s", + family, net->net_value); + snprintf(path, sizeof(path), + "/etc/hostname.%s", ifname); + fileout(line, "a", path); + + /* runtime configuration */ + (void)shell("ifconfig", ifname, family, + "alias", net->net_value, NULL); + break; + case NET_GATEWAY: + fileout(net->net_value, "a", "/etc/mygate"); + break; + case NET_DNS: + snprintf(line, sizeof(line), "nameserver %s", + net->net_value); + fileout(line, "a", "/etc/resolv.conf"); + break; + default: + break; + } + } + + return (0); +} + void agent_unconfigure(void) { @@ -924,13 +1103,18 @@ main(int argc, char *const *argv) * XXX Detect cloud with help from hostctl and sysctl * XXX in addition to the interface name. */ - if (strcmp("hvn0", sc->sc_interface) == 0) + if (opennebula(sc) == 0) + ret = 0; + else if (strcmp("hvn0", sc->sc_interface) == 0) ret = azure(sc); else if (strcmp("xnf0", sc->sc_interface) == 0) ret = ec2(sc); else ret = openstack(sc); + if (sc->sc_stack != NULL) + log_debug("%s: %s", __func__, sc->sc_stack); + if (sc->sc_dryrun) { agent_free(sc); return (0); diff --git a/agent/main.h b/agent/main.h index fb2873a..7fdaf30 100644 --- a/agent/main.h +++ b/agent/main.h @@ -43,7 +43,31 @@ struct ssh_pubkey { }; TAILQ_HEAD(ssh_pubkeys, ssh_pubkey); +enum net_type { + NET_IP, + NET_MASK, + NET_PREFIX, + NET_MAC, + NET_MTU, + NET_GATEWAY, + NET_DNS, + NET_MAX +}; + +struct net_addr { + enum net_type net_type; + unsigned int net_ifunit; + char *net_value; + struct sockaddr_storage net_addr; + unsigned int net_num; + + TAILQ_ENTRY(net_addr) net_entry; +}; +TAILQ_HEAD(net_addrs, net_addr); + struct system_config { + const char *sc_stack; + char *sc_hostname; char *sc_username; char *sc_password; @@ -56,10 +80,15 @@ struct system_config { const char *sc_ovfenv; const char *sc_interface; const char *sc_cdrom; + int sc_mount; struct source sc_addr; struct ssh_pubkeys sc_pubkeys; + int sc_network; + struct net_addrs sc_netaddrs; + unsigned int sc_netmtu; + int sc_nullfd; int sc_dryrun; void *sc_priv; @@ -92,6 +121,9 @@ int azure(struct system_config *); int ec2(struct system_config *); int cloudinit(struct system_config *); +/* opennebula.c */ +int opennebula(struct system_config *); + /* openstack.c */ int openstack(struct system_config *); @@ -105,6 +137,10 @@ char *get_line(const unsigned char *, size_t); char *get_word(const unsigned char *, size_t); int agent_addpubkey(struct system_config *, const char *, const char *); int agent_setpubkey(struct system_config *, const char *, const char *); +struct net_addr * + agent_getnetaddr(struct system_config *, struct net_addr *); +int agent_addnetaddr(struct system_config *, unsigned int, + const char *, int, enum net_type); char *metadata(struct system_config *, const char *, enum strtype); char *metadata_file(struct system_config *, const char *, enum strtype); int connect_wait(int, const struct sockaddr *, socklen_t); diff --git a/agent/opennebula.c b/agent/opennebula.c new file mode 100644 index 0000000..1372537 --- /dev/null +++ b/agent/opennebula.c @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2018 Reyk Floeter + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "main.h" + +int +opennebula(struct system_config *sc) +{ + FILE *fp; + const char *delim = "\\\\\0", *errstr; + char *line = NULL, *k, *v, *p, q; + char *hname = NULL; + size_t len, lineno = 0, i; + int ret = -1; + unsigned int unit; + + /* Return silently without error */ + if ((fp = fopen("/mnt/context.sh", "r")) == NULL) + goto done; + + sc->sc_stack = "opennebula"; + + while ((line = fparseln(fp, &len, &lineno, + delim, FPARSELN_UNESCALL)) != NULL) { + /* key */ + k = line; + + /* a context always starts with this header */ + if (lineno == 1) { + ret = strcmp(line, + "# Context variables generated by OpenNebula"); + if (ret != 0) { + log_debug("%s: unsupported context", __func__); + goto done; + } + free(line); + continue; + } + line[strcspn(line, "#")] = '\0'; + + /* value */ + if ((v = strchr(line, '=')) == NULL || *(v + 1) == '\0') { + free(line); + continue; + } + *v++ = '\0'; + + /* value is quoted */ + q = *v; + if (strspn(v, "\"'") == 0 || (p = strrchr(v, q)) == v) { + free(line); + continue; + } + *v++ = '\0'; + *p = '\0'; + + /* continue if value is empty */ + if (*v == '\0') { + free(line); + continue; + } + + log_debug("%s: %s = %s", __func__, k, v); + + if (strcasecmp("NETWORK", k) == 0) { + if (strcasecmp("YES", v) == 0) + sc->sc_network = 1; + else if (strcasecmp("YES", v) == 0) + sc->sc_network = 0; + } else if (fnmatch("ETH*_*", k, 0) != FNM_NOMATCH) { + /* Extract interface unit */ + if ((p = strdup(k + 3)) == NULL) { + log_debug("%s: %s", __func__, k); + goto done; + } + p[strcspn(p, "_")] = '\0'; + unit = strtonum(p, 0, UINT32_MAX, &errstr); + free(p); + if (errstr != NULL) { + log_debug("%s: %s", __func__, k); + goto done; + } + + /* Get subkey */ + k += strcspn(k, "_") + 1; + + if (strcasecmp("DNS", k) == 0) { + /* We don't support per-interface DNS */ + for (p = v; *p != '\0'; v = p) { + p = v + strcspn(v, " \t"); + *p++ = '\0'; + if ((ret = agent_addnetaddr(sc, 0, + v, AF_UNSPEC, NET_DNS)) != 0) + break; + } + } else if (strcasecmp("IP", k) == 0) { + ret = agent_addnetaddr(sc, unit, + v, AF_INET, NET_IP); + } else if (strcasecmp("MASK", k) == 0) { + ret = agent_addnetaddr(sc, unit, + v, AF_INET, NET_MASK); + } else if (strcasecmp("GATEWAY", k) == 0) { + ret = agent_addnetaddr(sc, unit, + v, AF_INET, NET_GATEWAY); + } else if (strcasecmp("IP6", k) == 0) { + ret = agent_addnetaddr(sc, unit, + v, AF_INET6, NET_IP); + } else if (strcasecmp("GATEWAY6", k) == 0) { + ret = agent_addnetaddr(sc, unit, + v, AF_INET6, NET_GATEWAY); + } else if (strcasecmp("PREFIX_LENGTH", k) == 0) { + ret = agent_addnetaddr(sc, unit, + v, AF_INET6, NET_PREFIX); + } else if (strcasecmp("MAC", k) == 0) { + if (unit == 0 && hname == NULL) { + /* Fake a hostname using the mac */ + if ((hname = p = calloc(1, + strlen(v) + 3)) == NULL) { + log_debug("%s: calloc", + __func__); + goto done; + } + *p++ = 'v'; + *p++ = 'm'; + for (i = 0; i < strlen(v); i++) { + if (!isalnum(v[i])) + continue; + *p++ = v[i]; + } + } + + ret = agent_addnetaddr(sc, unit, + v, AF_UNSPEC, NET_MAC); + } else if (strcasecmp("MTU", k) == 0) { + ret = agent_addnetaddr(sc, unit, + v, AF_UNSPEC, NET_MTU); + } else + ret = 0; + if (ret != 0) { + log_debug("%s: failed to parse %s", + __func__, k); + goto done; + } + } else if (strcasecmp("SSH_PUBLIC_KEY", k) == 0) { + if (agent_addpubkey(sc, v, NULL) != 0) + log_warnx("failed to ssh pubkey"); + } + + free(line); + } + + fclose(fp); + fp = NULL; + + /* + * OpenNebula doesn't provide an instance id so we + * calculate one using the hash of the context file. + * This might break if the context is not consistent. + */ + if ((sc->sc_instance = + calloc(1, SHA256_DIGEST_STRING_LENGTH)) == NULL || + SHA256File("/mnt/context.sh", sc->sc_instance) == NULL) { + log_debug("%s: failed to calculate instance hash", + __func__); + goto done; + } + log_debug("%s: context instance %s", __func__, sc->sc_instance); + + /* Even the hostname is optional */ + if (hname != NULL) { + free(sc->sc_hostname); + sc->sc_hostname = hname; + log_debug("%s: hostname %s", __func__, hname); + } + + line = NULL; + ret = 0; + + done: + if (fp != NULL) + fclose(fp); + free(line); + return (ret); +} diff --git a/agent/openstack.c b/agent/openstack.c index a060c3d..9c84d34 100644 --- a/agent/openstack.c +++ b/agent/openstack.c @@ -44,6 +44,7 @@ openstack(struct system_config *sc) free(sc->sc_endpoint); return (cloudinit(sc)); } + return (0); } @@ -62,6 +63,7 @@ openstack_fetch(struct system_config *sc) if ((json = metadata(sc, "/openstack/latest/meta_data.json", TEXT)) == NULL) goto fail; + sc->sc_stack = "openstack"; if ((j = json_parse(json, strlen(json))) == NULL) goto fail;