Compare commits

...

44 Commits
v0.2 ... master

Author SHA1 Message Date
reykfloeter 14d0149cdb CMS is back! Use it in LibreSSL > 3.0.2 or with USE_LIBRESSL_CMS=1 2019-11-29 17:22:07 +00:00
reykfloeter beef9f736c Append /etc/resolv.conf.tail if it exists 2019-11-29 17:06:39 +00:00
reykfloeter 0b1fee287c I forgot -c in the usage, will be part of the next release 2019-06-28 16:07:06 +02:00
reykfloeter c4595717fb Finalize version 0.9 2019-06-26 14:42:12 +02:00
reykfloeter e800a2b7d2 Add support OpenNebula's USERNAME 2019-06-14 09:45:48 +02:00
reykfloeter 39f33c76a4 Add Changelog, tweak README.md 2019-06-11 15:11:28 +02:00
reykfloeter fd7fa10f1b Update copyright 2019-06-11 12:29:08 +02:00
reykfloeter c968f169fd Tweak previous, sync cloud-agent.md 2019-06-11 12:23:36 +02:00
reykfloeter 678b29acef
Merge pull request #7 from reyk/probe-order
Allow to specify the probed cloud stacks with -c cloud[,cloud...]
2019-06-11 12:18:02 +02:00
reykfloeter ca22cba8e4 Allow to specify the probed cloud stacks with -c cloud[,cloud...] 2019-06-07 11:58:09 +02:00
reykfloeter ba34eb76dd Add cloud-agent(8) as markdown file 2019-06-05 22:12:42 +02:00
reykfloeter 3be9707418 Remove /var/db/cloud-instance file on -u 2019-06-05 22:01:21 +02:00
reykfloeter b8ae4a13fc Allow to generate and write a comment into ~/.ssh/authorized_keys 2019-06-05 21:39:05 +02:00
reykfloeter c5c1705cd7 Add support for OpenNebula's userdata START_SCRIPT and START_SCRIPT_BASE64 2019-06-05 18:18:49 +02:00
reykfloeter 99f8b2d2b0 The SSH_PUBLIC_KEY line in OpenNebula's context can span multiple lines 2019-06-04 15:15:24 +02:00
reykfloeter 0509d8d619 Only grow, but never shrink the disk 2019-06-02 11:22:57 +02:00
reykfloeter bce8634bf5 re-mount updated filesystem 2019-06-02 03:25:40 +02:00
reykfloeter 95e8cb1cb1 Fix bEnd 2019-06-02 03:21:22 +02:00
reykfloeter 63935a1b5f Don't print errors from the DHCP endpoint 2019-06-02 03:15:18 +02:00
reykfloeter 139f35d9d4 Only update disk if size was changed and not under dryrun 2019-06-02 03:04:25 +02:00
reykfloeter aa963100ba Fall back to 169.254.169.254 if DHCP endpoint does not work (unbreak OpenStack) 2019-06-02 02:41:36 +02:00
reykfloeter 8dfa3c843a Add "-r rootdisk" growdisk support 2019-06-01 23:41:49 +02:00
reykfloeter a8490a757f Escape backslash in mdoc 2019-06-01 17:01:29 +02:00
reykfloeter cf0be19caa
Merge pull request #5 from reyk/fix-cms
Unbreak cloud-agent's cms build on newer LibreSSL (OpenBSD 6.5)

OK with feedback from tb@openbsd
2019-05-10 12:11:28 +02:00
reykfloeter ffe93c5b4f Add #if defined(LIBRESSL_VERSION_NUMBER) 2019-05-10 12:09:59 +02:00
reykfloeter 3e3c5d914e Unbreak cloud-agent's cms build on newer LibreSSL (OpenBSD 6.5)
Fix from github.com/xenotrope
2019-05-10 11:33:15 +02:00
reykfloeter 3290c27210 If root, don't overwrite doas.conf 2018-08-15 13:48:51 +02:00
reykfloeter 20e2f78f83 Add -U option to overwrite the user 2018-08-15 12:27:19 +02:00
reykfloeter 91eb82f902 Make the group egress optional (dynamic) in the initial pf rule 2018-08-15 11:46:21 +02:00
reykfloeter 333f7ac6d7 Write network configuration files 2018-08-14 09:55:17 +02:00
reykfloeter d9899d488a Add initial support for OpenNebula contextualization.
Thanks to datacenterlight.ch by ungleich glarus AG for providing
access to their OpenNebula-based cloud.
2018-08-14 00:34:25 +02:00
reykfloeter ec87db177d Revert LDADD to -ltls -lssl -lcrypto as just -ltls breaks static linking. 2018-05-16 12:28:49 +02:00
reykfloeter bc8d60d5f6
Update README.md, remove warning, mention more clouds 2018-05-15 23:37:46 +02:00
reykfloeter daec249cc0 Fix previous 2018-05-15 23:16:57 +02:00
reykfloeter bf8bfd607b Fix build with LibreSSL 2.8 and constified the ASN1_OBJECTs. 2018-05-15 23:11:32 +02:00
reykfloeter 8c4f6a384b /var/run is cleared on boot, store user-data in /etc instead 2018-05-08 12:24:32 +02:00
reykfloeter 5de42d6464 user-data is not always base64-encoded. 2018-05-08 12:08:58 +02:00
Reyk Floeter f48b2bc2b9 Make the public key optional but print a warning 2018-05-08 10:03:38 +02:00
Reyk Floeter 155d216845 Add support for user-data scripts 2018-05-08 09:51:32 +02:00
Reyk Floeter 73f066699f Only link to libtls (which will pull libssl/crypto by itself) 2018-05-08 09:48:59 +02:00
Reyk Floeter cb7edf897e Fix return value on error 2018-05-08 09:48:30 +02:00
Reyk Floeter 4953fa418c Rename label 2018-05-08 09:48:08 +02:00
Reyk Floeter 9ffe04f62f Fall back to meta-data/public-keys if meta-data/public-keys/0/openssh-key is not available. 2018-05-07 18:10:27 +02:00
Reyk Floeter eaa8b96541 Add missing json files 2018-05-07 12:23:02 +02:00
20 changed files with 2315 additions and 151 deletions

69
CHANGELOG.md Normal file
View File

@ -0,0 +1,69 @@
# Changelog
## v1.0 (2019-11-29)
* Append `/etc/resolv.conf.tail` to `/etc/resolv.conf` if it exists.
* Fixed usage.
## v0.9 (2019-06-26)
* Added support for `-c` to specify the probing order of different cloud stacks.
* Added support for OpenNebula's START_SCRIPT methods (user-data alike).
* Added support for OpenNebula's USERNAME method.
* Added support for generating a default user password and writing it into
`~/.ssh/authorized_keys`.
* Fixed handling of OpenNebula SSH_PUBLIC_KEY entries with multiple keys.
* Improved documentation, added `CHANGELOG.md` file.
## v0.8 (2019-06-02)
* Added support for growing the root disk and its last partition (optional).
* Fixed OpenStack support.
* Fixed compilation with LibreSSL on OpenBSD 6.5 or newer.
* Fixed probing order and OpenStack with `169.254.169.254` as the endpoint IP.
* Improved OpenNebula support.
## v0.7 (2018-08-15)
* Added initial support for OpenNebula contextualization.
* Added support for setting a custom login user or "root" with `-U`.
* Added support for writing `resolv.conf` and static network configuration.
* Fixed the generated pf rule that is loaded during cloud-agent operation.
## v0.6 (2018-05-15)
* Fixed compilation with (old) OpenSSL releases.
---
## v0.5 (2018-05-08)
* Fixed the user-data script by loading it from /etc/rc.user-data.
## v0.4 (2018-05-08)
* Added support for user-data that is not base64-encoded.
## v0.3 (2018-05-08)
* Added support for user-data scripts.
* Make the public key optional for stacks that supply a password (e.g. Azure).
## v0.2.2 (2018-05-07)
* Fixed issues in the v0.2.1 release.
## v0.2.2 (2018-05-07)
* Fixed issues in the v0.2 release.
## v0.2 (2018-01-10)
* Added support for OpenStack and its JSON-based meta data.
* Added support for Apache CloudStack.
* Try to get meta data from `dhcp-server-identifier` instead of
`169.254.169.254`.
## v0.1 (2017-07-03)
* Initial release with support for Microsoft Azure and Amazon AWS EC2.

View File

@ -6,10 +6,10 @@ License
* `cloud-agent` is free software under OpenBSD's ISC-style license.
* Most of the code has been written by Reyk Floeter <reyk@openbsd.org>
* The http.[ch] files have been written by Kristaps Dzonsons <kristaps@bsd.lv>
* The {http,json}.[ch] files were written by Kristaps Dzonsons <kristaps@bsd.lv>
* Please refer to the individual source files for other copyright holders!
> Copyright (c) 2017 Reyk Floeter <reyk@openbsd.org>
> Copyright (c) 2017, 2018, 2019 Reyk Floeter <reyk@openbsd.org>
>
> Permission to use, copy, modify, and distribute this software for any
> purpose with or without fee is hereby granted, provided that the above
@ -26,5 +26,5 @@ License
`cms/`
------
* The CMS code is from the OpenSSL and/or LibreSSL.
* The CMS code is from OpenSSL and/or LibreSSL.
* Please refer to the individual source files for other copyright holders!

View File

@ -1,10 +1,13 @@
#
# The Azure agents needs CMS to obtain the SSH public keys.
# LibreSSL has removed CMS, so either use OpenSSL to decrypt CMS
# messages or compile the old CMS code for LibreSSL.
# messages or compile the old CMS code for LibreSSL. Or use
# CMS that has returned to newer versions of LibreSSL.
#
.ifdef USE_OPENSSL
MAKE_FLAGS+= USE_OPENSSL=1
.elifdef USE_LIBRESSL_CMS
MAKE_FLAGS+= USE_LIBRESSL_CMS=1
.else
SUBDIR= cms
.endif

View File

@ -1,8 +1,6 @@
cloud-agent for OpenBSD
=======================
**This is just experimental. Be warned.**
This is a simple OpenBSD-specific agent that aims to handle
provisioning and cloud initialization on public clouds such as
Microsoft Azure and Amazon AWS. For OpenBSD on Azure, it is a minimal
@ -25,19 +23,40 @@ has removed CMS which is required by Azure.
Usage
-----
Installation is easy, `cloud-agent` detects the cloud type automatically.
See the [cloud-agent(8)](cloud-agent.md) documentation for more
information about the usage.
Basic installation is easy, `cloud-agent` detects the cloud type
automatically.
* On Microsoft Azure, create a file `/etc/hostname.hvn0`
* On Amazon AWS, create a file `/etc/hostname.xnf0`
* On CloudStack, such as Exoscale, create a file `/etc/hostname.vio0`
* On OpenBSD VMM (with meta-data), create a file `/etc/hostname.vio0`
* The content of the file is identical for all of them:
* On OpenStack/VMware, create a file `/etc/hostname.vmx0`
* The content of the file is identical for all of the above:
dhcp
!/usr/local/libexec/cloud-agent "\$if"
* On OpenNebula, such as Data Center Light, create a file `/etc/hostname.if`
where _if_ is the name of your primary interface.
The `dhcp` line should be ommitted in the file:
!/usr/local/libexec/cloud-agent "\$if"
Releases
--------
See the [Changelog](CHANGELOG.md) for a summary of changes and
download the releases from the
[release page](https://github.com/reyk/cloud-agent/releases).
Author
------

View File

@ -1,5 +1,7 @@
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
SRCS+= growdisk.c
BINDIR= /usr/local/libexec
MANDIR= /usr/local/man/man
@ -15,7 +17,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 <bsd.prog.mk>

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2017 Reyk Floeter <reyk@openbsd.org>
* Copyright (c) 2017, 2018, 2019 Reyk Floeter <reyk@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@ -23,8 +23,11 @@
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
#include <err.h>
#include <openssl/opensslv.h>
#include "main.h"
#include "http.h"
#include "xml.h"
@ -62,55 +65,62 @@ azure(struct system_config *sc)
{
int ret = -1;
/* Apply defaults */
free(sc->sc_username);
if ((sc->sc_username = strdup("azure-user")) == NULL) {
log_warnx("failed to set default user");
goto done;
if (sc->sc_state == STATE_INIT) {
free(sc->sc_username);
if ((sc->sc_username = strdup("azure-user")) == NULL) {
log_warnx("failed to set default user");
return (-1);
}
/* Apply defaults */
sc->sc_ovfenv = "/var/db/azure-ovf-env.xml";
sc->sc_priv = &az_config;
sc->sc_state = STATE_DHCP;
return (-1);
}
sc->sc_cdrom = "/dev/cd0c";
sc->sc_ovfenv = "/var/db/azure-ovf-env.xml";
sc->sc_priv = &az_config;
/* Don't try other endpoints */
sc->sc_state = STATE_DONE;
if (azure_getovfenv(sc) != 0) {
log_warnx("failed to get ovf-env.xml");
goto done;
goto fail;
}
if (dhcp_getendpoint(sc) != 0) {
log_warnx("failed to get endpoint");
goto done;
goto fail;
}
if (azure_versions(sc) != 0) {
log_warnx("failed to get endpoint versions");
goto done;
goto fail;
}
if (azure_goalstate(sc) != 0) {
log_warnx("failed to get goalstate");
goto done;
goto fail;
}
if (!sc->sc_dryrun) {
if (azure_keys(sc) != 0) {
log_warnx("failed to get transport keys");
goto done;
goto fail;
}
if (azure_certificates(sc) != 0) {
log_warnx("failed to get certificates");
goto done;
goto fail;
}
}
if (azure_reporthealth(sc, "Ready") != 0) {
log_warnx("failed to report health");
goto done;
goto fail;
}
ret = 0;
done:
fail:
free(az_config.az_container);
free(az_config.az_pubkeyval);
@ -433,13 +443,16 @@ azure_certificates(struct system_config *sc)
fd = disable_output(sc, STDERR_FILENO);
#ifdef USE_OPENSSL
#if defined(USE_OPENSSL)
/*
* XXX Now comes the part that needs CMS which is only
* XXX present in OpenSSL but got removed from LibreSSL.
*/
log_debug("%s: running openssl cms", __func__);
if (shell("/usr/local/bin/eopenssl", "cms", /* )) */
#elif defined(USE_LIBRESSL_CMS) || LIBRESSL_VERSION_NUMBER > 0x3000200fL
/* And CMS returned to LibreSSL! */
if (shell("/usr/bin/openssl", "cms", /* )) */
#else
if (shell("/usr/local/bin/cms",
#endif
@ -654,36 +667,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) {
@ -732,7 +731,7 @@ azure_getovfenv(struct system_config *sc)
}
if ((xe = xml_findl(&xp->xe_head, "UserPassword", NULL)) != NULL) {
if ((sc->sc_password = calloc(1, 128)) == NULL) {
if ((sc->sc_password_hash = calloc(1, _PASSWORD_LEN)) == NULL) {
log_debug("%s: password failed", __func__);
goto done;
}
@ -740,13 +739,14 @@ azure_getovfenv(struct system_config *sc)
str = strndup(xe->xe_data, xe->xe_datalen);
if (str == NULL ||
crypt_newhash(str, "bcrypt,a",
sc->sc_password, 128) != 0) {
sc->sc_password_hash, _PASSWORD_LEN) != 0) {
log_debug("%s: password hashing failed", __func__);
free(sc->sc_password);
sc->sc_password = NULL;
free(sc->sc_password_hash);
sc->sc_password_hash = NULL;
free(str);
goto done;
}
explicit_bzero(str, xe->xe_datalen);
free(str);
/* Replace unencrypted password with hash */
@ -756,11 +756,11 @@ azure_getovfenv(struct system_config *sc)
/* Update element for xml_print() below */
explicit_bzero(xe->xe_data, xe->xe_datalen);
free(xe->xe_data);
xe->xe_data = strdup(sc->sc_password);
xe->xe_datalen = strlen(sc->sc_password);
xe->xe_data = strdup(sc->sc_password_hash);
xe->xe_datalen = strlen(sc->sc_password_hash);
} else if ((xe = xml_findl(&xp->xe_head,
"UserPasswordHash", NULL)) != NULL) {
if ((sc->sc_password =
if ((sc->sc_password_hash =
get_word(xe->xe_data, xe->xe_datalen)) != NULL) {
log_debug("%s: password hash failed", __func__);
goto done;

View File

@ -1,6 +1,6 @@
.\" $OpenBSD: mdoc.template,v 1.15 2014/03/31 00:09:54 dlg Exp $
.\"
.\" Copyright (c) 2017 Reyk Floeter <reyk@openbsd.org>
.\" Copyright (c) 2017, 2018, 2019 Reyk Floeter <reyk@openbsd.org>
.\"
.\" Permission to use, copy, modify, and distribute this software for any
.\" purpose with or without fee is hereby granted, provided that the above
@ -23,17 +23,75 @@
.Sh SYNOPSIS
.Nm cloud-agent
.Op Fl nuv
.Op Fl c Ar cloud Ns Op , Ns Ar cloud Ns ...
.Op Fl p Ar length
.Op Fl r Ar rootdisk
.Op Fl t Ar timeout
.Op Fl U Ar username
.Ar interface
.Sh DESCRIPTION
The
.Nm
program manages the OpenBSD provisioning and VM interaction in cloud
environments, including Microsoft Azure and Amazon AWS.
environments.
.Pp
The options are as follows:
.Bl -tag -width Ds
.It Fl c Ar cloud Ns Op , Ns Ar cloud Ns ...
Probe a list of cloud stacks for provisioning in the specified order.
If this option is not specified,
.Nm
tries to detect the environment and possible cloud stacks automatically.
Supported
.Ar cloud
stacks are:
.Pp
.Bl -tag -width opennebula -offset indent -compact
.It Ic azure
Microsoft Azure
.It Ic cloudinit
Generic cloud-init
.It Ic ec2
Amazon AWS EC2
.It Ic opennebula
OpenNebula
.It Ic openstack
OpenStack
.El
.It Fl p Ar length
Generate and set a random password for the default user.
The password will be written in its plain form into the
.Pa ~/.ssh/authorized_keys
file.
This allows to use the
.Xr doas 1
command to gain root privileges.
The minimum
.Ar length
is 8 characters and the default is an empty password.
.It Fl n
Do not configure the system and skip the provisioning step.
.It Fl t Ar timeout
Change the HTTP timeout.
The default is 3 seconds.
.It Fl U Ar username
Change the default user.
The default is
.Dq ec2-user
on AWS,
.Dq azure-user
on Azure, and
.Dq puffy
everywhere else.
The default user is used when it is not obtained from the cloud
configuration.
Using
.Dq root
is supported, but not recommended.
.It Fl r Ar rootdisk
Automatically grow the last
.Ox
FFS partition of the root disk to use all the available space.
.It Fl u
Deprovision and unconfigure the system.
This deletes keys, passwords, and logs files without asking for permission.
@ -45,14 +103,17 @@ Enable
.Nm
in the
.Xr hostname.if 5
of the VM's primary networking interface:
of the VM's primary networking interface and automatically the last
partition of the root disk:
.Bd -literal -offset indent
# cat /etc/hostname.hvn0
dhcp
!/usr/local/libexec/cloud-agent "\$if"
!/usr/local/libexec/cloud-agent -r sd0 "\e$if"
.Ed
.Sh FILES
.Bl -tag -width "/usr/local/libexec/cloud-agentX" -compact
.It Pa ~/.ssh/authorized_keys
The location of the agent-configured SSH public keys and optional password.
.It Pa /usr/local/libexec/cloud-agent
The agent itself.
.It Pa /usr/local/bin/cms

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2017 Reyk Floeter <reyk@openbsd.org>
* Copyright (c) 2017, 2018, 2019 Reyk Floeter <reyk@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@ -34,32 +34,32 @@ static int cloudinit_fetch(struct system_config *);
int
ec2(struct system_config *sc)
{
free(sc->sc_username);
if ((sc->sc_username = strdup("ec2-user")) == NULL ||
(sc->sc_endpoint = strdup(DEFAULT_ENDPOINT)) == NULL) {
log_warnx("failed to set defaults");
if (sc->sc_state == STATE_INIT) {
free(sc->sc_username);
if ((sc->sc_username = strdup("ec2-user")) == NULL) {
log_warnx("failed to set default user");
return (-1);
}
sc->sc_state = STATE_169;
return (-1);
}
return (cloudinit_fetch(sc));
return cloudinit_fetch(sc);
}
int
cloudinit(struct system_config *sc)
{
if ((dhcp_getendpoint(sc) == -1) &&
(sc->sc_endpoint = strdup(DEFAULT_ENDPOINT)) == NULL) {
log_warnx("failed to set defaults");
if (sc->sc_state == STATE_INIT) {
sc->sc_state = STATE_DHCP;
return (-1);
}
return (cloudinit_fetch(sc));
return cloudinit_fetch(sc);
}
static int
cloudinit_fetch(struct system_config *sc)
{
int ret = 0;
int ret = -1;
char *str = NULL;
sc->sc_addr.ip = sc->sc_endpoint;
@ -75,11 +75,13 @@ cloudinit_fetch(struct system_config *sc)
"/latest/meta-data/local-hostname", WORD)) == NULL)
goto fail;
/* pubkey */
/* optional pubkey */
if ((str = metadata(sc,
"/latest/meta-data/public-keys/0/openssh-key", LINE)) == NULL)
goto fail;
if (agent_addpubkey(sc, str, NULL) != 0)
"/latest/meta-data/public-keys/0/openssh-key", LINE)) == NULL &&
(str = metadata(sc,
"/latest/meta-data/public-keys", LINE)) == NULL)
log_warnx("failed to get public key");
else if (agent_addpubkey(sc, str, NULL) != 0)
goto fail;
/* optional username - this is an extension by meta-data(8) */

173
agent/growdisk.c Normal file
View File

@ -0,0 +1,173 @@
/*
* Copyright (c) 2019 Reyk Floeter <reyk@openbsd.org>
*
* 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 <sys/param.h> /* DEV_BSIZE */
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/dkio.h>
#define DKTYPENAMES
#include <sys/disklabel.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <util.h>
#include <err.h>
#include "main.h"
#define MEG(_n) ((_n) * DEV_BSIZE / 1024 / 1024)
static uint16_t dkcksum(struct disklabel *);
static uint16_t
dkcksum(struct disklabel *lp)
{
uint16_t *start, *end, sum;
start = (uint16_t *)lp;
end = (uint16_t *)&lp->d_partitions[lp->d_npartitions];
for (sum = 0; start < end;)
sum ^= *start++;
return (sum);
}
int
growdisk(struct system_config *sc)
{
char c, last_part = 0, *out = NULL, *path = NULL;
int ret = -1, i, errfd, outfd;
uint64_t bend, psize;
struct partition *pp, *p = NULL;
struct disklabel lp;
uint16_t cksum;
int fd;
/*
* Grow the OpenBSD MBR partition
*/
/* XXX this is a bit ugly but easier to do */
if (!sc->sc_dryrun &&
shellout("e 3\n\n\n\n*\nw\nq\n", &out,
"fdisk", "-e", sc->sc_rootdisk, NULL) != 0) {
log_warnx("failed to grow OpenBSD partition");
return (-1);
}
free(out);
/*
* Grow the last partition in the disklabel
*/
if ((fd = opendev(sc->sc_rootdisk,
O_RDWR, OPENDEV_PART, NULL)) == -1) {
log_warn("failed to open %s", sc->sc_rootdisk);
return (-1);
}
if (ioctl(fd, DIOCGDINFO, &lp) == -1) {
log_warn("failed to get disklabel");
goto done;
}
if (lp.d_magic != DISKMAGIC || lp.d_magic2 != DISKMAGIC) {
log_warnx("invalid disklabel magic bytes");
goto done;
}
cksum = lp.d_checksum;
lp.d_checksum = 0;
if (dkcksum(&lp) != cksum) {
log_warnx("invalid disklabel checksum");
goto done;
}
pp = lp.d_partitions;
for (i = 0, pp = lp.d_partitions; i < lp.d_npartitions; i++, pp++) {
if (!DL_GETPSIZE(pp))
continue;
c = 'a' + i;
if (pp->p_fstype == FS_BSDFFS) {
last_part = c;
p = pp;
}
}
if (last_part == 0) {
log_warnx("last BSD partition not found");
goto done;
}
bend = DL_GETDSIZE(&lp) - DL_GETBSTART(&lp);
psize = bend - DL_GETPOFFSET(p);
/* Only grow, but never shring the disk */
if (bend <= DL_GETBEND(&lp) && psize <= DL_GETPSIZE(p)) {
log_debug("%s: not growing %s%c, size is %lluMB",
__func__, sc->sc_rootdisk, last_part, MEG(psize));
ret = 0;
} else {
log_info("growing %s%c from %lluMB to %lluMB",
sc->sc_rootdisk, last_part,
MEG(DL_GETPSIZE(p)), MEG(psize));
ret = -1;
}
if (sc->sc_dryrun || ret == 0) {
ret = 0;
goto done;
}
/* Update OpenBSD boundaries */
DL_SETBEND(&lp, bend);
/* Update the size of the last partition */
DL_SETPSIZE(p, psize);
lp.d_checksum = dkcksum(&lp);
if (ioctl(fd, DIOCWDINFO, &lp) == -1) {
log_warn("failed to write disklabel");
goto done;
}
/*
* Grow the filesystem
*/
if (asprintf(&path, "/dev/%s%c", sc->sc_rootdisk, last_part) == -1)
goto done;
errfd = disable_output(sc, STDERR_FILENO);
outfd = disable_output(sc, STDOUT_FILENO);
(void)shell("umount", "-f", path, NULL);
(void)shell("growfs", "-yq", path, NULL);
if ((ret = shell("fsck", "-y", path, NULL)) != 0)
ret = -1;
(void)shell("mount", "-a", "-t", "nonfs,vnd", NULL);
enable_output(sc, STDERR_FILENO, errfd);
enable_output(sc, STDOUT_FILENO, outfd);
ret = 0;
done:
free(path);
close(fd);
return (ret);
}

332
agent/jsmn.c Normal file
View File

@ -0,0 +1,332 @@
/*
Copyright (c) 2010 Serge A. Zaitsev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.*
*/
#include "jsmn.h"
/**
* Allocates a fresh unused token from the token pull.
*/
static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser,
jsmntok_t *tokens, size_t num_tokens) {
jsmntok_t *tok;
if (parser->toknext >= num_tokens) {
return NULL;
}
tok = &tokens[parser->toknext++];
tok->start = tok->end = -1;
tok->size = 0;
#ifdef JSMN_PARENT_LINKS
tok->parent = -1;
#endif
return tok;
}
/**
* Fills token type and boundaries.
*/
static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type,
int start, int end) {
token->type = type;
token->start = start;
token->end = end;
token->size = 0;
}
/**
* Fills next available token with JSON primitive.
*/
static int jsmn_parse_primitive(jsmn_parser *parser, const char *js,
size_t len, jsmntok_t *tokens, size_t num_tokens) {
jsmntok_t *token;
int start;
start = parser->pos;
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
switch (js[parser->pos]) {
#ifndef JSMN_STRICT
/* In strict mode primitive must be followed by "," or "}" or "]" */
case ':':
#endif
case '\t' : case '\r' : case '\n' : case ' ' :
case ',' : case ']' : case '}' :
goto found;
}
if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
parser->pos = start;
return JSMN_ERROR_INVAL;
}
}
#ifdef JSMN_STRICT
/* In strict mode primitive must be followed by a comma/object/array */
parser->pos = start;
return JSMN_ERROR_PART;
#endif
found:
if (tokens == NULL) {
parser->pos--;
return 0;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
parser->pos = start;
return JSMN_ERROR_NOMEM;
}
jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
parser->pos--;
return 0;
}
/**
* Fills next token with JSON string.
*/
static int jsmn_parse_string(jsmn_parser *parser, const char *js,
size_t len, jsmntok_t *tokens, size_t num_tokens) {
jsmntok_t *token;
int start = parser->pos;
parser->pos++;
/* Skip starting quote */
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
char c = js[parser->pos];
/* Quote: end of string */
if (c == '\"') {
if (tokens == NULL) {
return 0;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
parser->pos = start;
return JSMN_ERROR_NOMEM;
}
jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos);
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
return 0;
}
/* Backslash: Quoted symbol expected */
if (c == '\\' && parser->pos + 1 < len) {
int i;
parser->pos++;
switch (js[parser->pos]) {
/* Allowed escaped symbols */
case '\"': case '/' : case '\\' : case 'b' :
case 'f' : case 'r' : case 'n' : case 't' :
break;
/* Allows escaped symbol \uXXXX */
case 'u':
parser->pos++;
for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) {
/* If it isn't a hex character we have an error */
if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */
(js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */
(js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */
parser->pos = start;
return JSMN_ERROR_INVAL;
}
parser->pos++;
}
parser->pos--;
break;
/* Unexpected symbol */
default:
parser->pos = start;
return JSMN_ERROR_INVAL;
}
}
}
parser->pos = start;
return JSMN_ERROR_PART;
}
/**
* Parse JSON string and fill tokens.
*/
int jsmn_parse(jsmn_parser *parser, const char *js, size_t len,
jsmntok_t *tokens, unsigned int num_tokens) {
int r;
int i;
jsmntok_t *token;
int count = parser->toknext;
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
char c;
jsmntype_t type;
c = js[parser->pos];
switch (c) {
case '{': case '[':
count++;
if (tokens == NULL) {
break;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL)
return JSMN_ERROR_NOMEM;
if (parser->toksuper != -1) {
tokens[parser->toksuper].size++;
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
}
token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
token->start = parser->pos;
parser->toksuper = parser->toknext - 1;
break;
case '}': case ']':
if (tokens == NULL)
break;
type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
#ifdef JSMN_PARENT_LINKS
if (parser->toknext < 1) {
return JSMN_ERROR_INVAL;
}
token = &tokens[parser->toknext - 1];
for (;;) {
if (token->start != -1 && token->end == -1) {
if (token->type != type) {
return JSMN_ERROR_INVAL;
}
token->end = parser->pos + 1;
parser->toksuper = token->parent;
break;
}
if (token->parent == -1) {
break;
}
token = &tokens[token->parent];
}
#else
for (i = parser->toknext - 1; i >= 0; i--) {
token = &tokens[i];
if (token->start != -1 && token->end == -1) {
if (token->type != type) {
return JSMN_ERROR_INVAL;
}
parser->toksuper = -1;
token->end = parser->pos + 1;
break;
}
}
/* Error if unmatched closing bracket */
if (i == -1) return JSMN_ERROR_INVAL;
for (; i >= 0; i--) {
token = &tokens[i];
if (token->start != -1 && token->end == -1) {
parser->toksuper = i;
break;
}
}
#endif
break;
case '\"':
r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
if (r < 0) return r;
count++;
if (parser->toksuper != -1 && tokens != NULL)
tokens[parser->toksuper].size++;
break;
case '\t' : case '\r' : case '\n' : case ' ':
break;
case ':':
parser->toksuper = parser->toknext - 1;
break;
case ',':
if (tokens != NULL && parser->toksuper != -1 &&
tokens[parser->toksuper].type != JSMN_ARRAY &&
tokens[parser->toksuper].type != JSMN_OBJECT) {
#ifdef JSMN_PARENT_LINKS
parser->toksuper = tokens[parser->toksuper].parent;
#else
for (i = parser->toknext - 1; i >= 0; i--) {
if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
if (tokens[i].start != -1 && tokens[i].end == -1) {
parser->toksuper = i;
break;
}
}
}
#endif
}
break;
#ifdef JSMN_STRICT
/* In strict mode primitives are: numbers and booleans */
case '-': case '0': case '1' : case '2': case '3' : case '4':
case '5': case '6': case '7' : case '8': case '9':
case 't': case 'f': case 'n' :
/* And they must not be keys of the object */
if (tokens != NULL && parser->toksuper != -1) {
jsmntok_t *t = &tokens[parser->toksuper];
if (t->type == JSMN_OBJECT ||
(t->type == JSMN_STRING && t->size != 0)) {
return JSMN_ERROR_INVAL;
}
}
#else
/* In non-strict mode every unquoted value is a primitive */
default:
#endif
r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
if (r < 0) return r;
count++;
if (parser->toksuper != -1 && tokens != NULL)
tokens[parser->toksuper].size++;
break;
#ifdef JSMN_STRICT
/* Unexpected char in strict mode */
default:
return JSMN_ERROR_INVAL;
#endif
}
}
if (tokens != NULL) {
for (i = parser->toknext - 1; i >= 0; i--) {
/* Unmatched opened object or array */
if (tokens[i].start != -1 && tokens[i].end == -1) {
return JSMN_ERROR_PART;
}
}
}
return count;
}
/**
* Creates a new parser based over a given buffer with an array of tokens
* available.
*/
void jsmn_init(jsmn_parser *parser) {
parser->pos = 0;
parser->toknext = 0;
parser->toksuper = -1;
}

97
agent/jsmn.h Normal file
View File

@ -0,0 +1,97 @@
/*
Copyright (c) 2010 Serge A. Zaitsev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.*
*/
#ifndef __JSMN_H_
#define __JSMN_H_
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* JSON type identifier. Basic types are:
* o Object
* o Array
* o String
* o Other primitive: number, boolean (true/false) or null
*/
typedef enum {
JSMN_UNDEFINED = 0,
JSMN_OBJECT = 1,
JSMN_ARRAY = 2,
JSMN_STRING = 3,
JSMN_PRIMITIVE = 4
} jsmntype_t;
enum jsmnerr {
/* Not enough tokens were provided */
JSMN_ERROR_NOMEM = -1,
/* Invalid character inside JSON string */
JSMN_ERROR_INVAL = -2,
/* The string is not a full JSON packet, more bytes expected */
JSMN_ERROR_PART = -3
};
/**
* JSON token description.
* @param type type (object, array, string etc.)
* @param start start position in JSON data string
* @param end end position in JSON data string
*/
typedef struct {
jsmntype_t type;
int start;
int end;
int size;
#ifdef JSMN_PARENT_LINKS
int parent;
#endif
} jsmntok_t;
/**
* JSON parser. Contains an array of token blocks available. Also stores
* the string being parsed now and current position in that string
*/
typedef struct {
unsigned int pos; /* offset in the JSON string */
unsigned int toknext; /* next token to allocate */
int toksuper; /* superior token node, e.g parent object or array */
} jsmn_parser;
/**
* Create JSON parser over an array of tokens
*/
void jsmn_init(jsmn_parser *parser);
/**
* Run JSON parser. It parses a JSON data string into and array of tokens, each describing
* a single JSON object.
*/
int jsmn_parse(jsmn_parser *parser, const char *js, size_t len,
jsmntok_t *tokens, unsigned int num_tokens);
#ifdef __cplusplus
}
#endif
#endif /* __JSMN_H_ */

319
agent/json.c Normal file
View File

@ -0,0 +1,319 @@
/* $OpenBSD: json.c,v 1.9 2017/01/24 13:32:55 jsing Exp $ */
/*
* Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv>
*
* 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 AUTHORS DISCLAIM ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS 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 <assert.h>
#include <err.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "jsmn.h"
#include "main.h"
struct jsmnp;
/*
* Objects consist of node pairs: the left-hand side (before the colon)
* and the right-hand side---the data.
*/
struct jsmnp {
struct jsmnn *lhs; /* left of colon */
struct jsmnn *rhs; /* right of colon */
};
/*
* Object for converting the JSMN token array into a tree.
*/
struct parse {
struct jsmnn *nodes; /* all nodes */
size_t cur; /* current number */
size_t max; /* nodes in "nodes" */
};
/*
* Recursive part for convertin a JSMN token array into a tree.
* See "example/jsondump.c" for its construction (it's the same except
* for how it handles allocation errors).
*/
static ssize_t
build(struct parse *parse, struct jsmnn **np,
jsmntok_t *t, const char *js, size_t sz)
{
size_t i, j;
struct jsmnn *n;
ssize_t tmp;
if (sz == 0)
return 0;
assert(parse->cur < parse->max);
n = *np = &parse->nodes[parse->cur++];
n->p = parse;
n->type = t->type;
switch (t->type) {
case JSMN_STRING:
/* FALLTHROUGH */
case JSMN_PRIMITIVE:
n->fields = 1;
n->d.str = strndup
(js + t->start,
t->end - t->start);
if (n->d.str == NULL)
break;
return 1;
case JSMN_OBJECT:
n->fields = t->size;
n->d.obj = calloc(n->fields,
sizeof(struct jsmnp));
if (n->d.obj == NULL)
break;
for (i = j = 0; i < (size_t)t->size; i++) {
tmp = build(parse,
&n->d.obj[i].lhs,
t + 1 + j, js, sz - j);
if (tmp < 0)
break;
j += tmp;
tmp = build(parse,
&n->d.obj[i].rhs,
t + 1 + j, js, sz - j);
if (tmp < 0)
break;
j += tmp;
}
if (i < (size_t)t->size)
break;
return j + 1;
case JSMN_ARRAY:
n->fields = t->size;
n->d.array = calloc(n->fields,
sizeof(struct jsmnn *));
if (n->d.array == NULL)
break;
for (i = j = 0; i < (size_t)t->size; i++) {
tmp = build(parse,
&n->d.array[i],
t + 1 + j, js, sz - j);
if (tmp < 0)
break;
j += tmp;
}
if (i < (size_t)t->size)
break;
return j + 1;
default:
break;
}
return -1;
}
/*
* Fully free up a parse sequence.
* This handles all nodes sequentially, not recursively.
*/
static void
jsmnparse_free(struct parse *p)
{
size_t i;
if (p == NULL)
return;
for (i = 0; i < p->max; i++) {
struct jsmnn *n = &p->nodes[i];
switch (n->type) {
case JSMN_ARRAY:
free(n->d.array);
break;
case JSMN_OBJECT:
free(n->d.obj);
break;
case JSMN_PRIMITIVE:
free(n->d.str);
break;
case JSMN_STRING:
free(n->d.str);
break;
case JSMN_UNDEFINED:
break;
}
}
free(p->nodes);
free(p);
}
/*
* Allocate a tree representation of "t".
* This returns NULL on allocation failure or when sz is zero, in which
* case all resources allocated along the way are freed already.
*/
static struct jsmnn *
jsmntree_alloc(jsmntok_t *t, const char *js, size_t sz)
{
struct jsmnn *first;
struct parse *p;
if (sz == 0)
return NULL;
p = calloc(1, sizeof(struct parse));
if (p == NULL)
return NULL;
p->max = sz;
p->nodes = calloc(p->max, sizeof(struct jsmnn));
if (p->nodes == NULL) {
free(p);
return NULL;
}
if (build(p, &first, t, js, sz) < 0) {
jsmnparse_free(p);
first = NULL;
}
return first;
}
/*
* Call through to free parse contents.
*/
void
json_free(struct jsmnn *first)
{
if (first != NULL)
jsmnparse_free(first->p);
}
/*
* Just check that the array object is in fact an object.
*/
struct jsmnn *
json_getarrayobj(struct jsmnn *n)
{
return n->type != JSMN_OBJECT ? NULL : n;
}
/*
* Extract an array from the returned JSON object, making sure that it's
* the correct type.
* Returns NULL on failure.
*/
struct jsmnn *
json_getarray(struct jsmnn *n, const char *name)
{
size_t i;
if (n->type != JSMN_OBJECT)
return NULL;
for (i = 0; i < n->fields; i++) {
if (n->d.obj[i].lhs->type != JSMN_STRING &&
n->d.obj[i].lhs->type != JSMN_PRIMITIVE)
continue;
else if (strcmp(name, n->d.obj[i].lhs->d.str))
continue;
break;
}
if (i == n->fields)
return NULL;
if (n->d.obj[i].rhs->type != JSMN_ARRAY)
return NULL;
return n->d.obj[i].rhs;
}
/*
* Extract a single string from the returned JSON object, making sure
* that it's the correct type.
* Returns NULL on failure.
*/
char *
json_getstr(struct jsmnn *n, const char *name)
{
size_t i;
char *cp;
if (n->type != JSMN_OBJECT)
return NULL;
for (i = 0; i < n->fields; i++) {
if (n->d.obj[i].lhs->type != JSMN_STRING &&
n->d.obj[i].lhs->type != JSMN_PRIMITIVE)
continue;
else if (strcmp(name, n->d.obj[i].lhs->d.str))
continue;
break;
}
if (i == n->fields)
return NULL;
if (n->d.obj[i].rhs->type != JSMN_STRING &&
n->d.obj[i].rhs->type != JSMN_PRIMITIVE)
return NULL;
cp = strdup(n->d.obj[i].rhs->d.str);
if (cp == NULL)
warn("strdup");
return cp;
}
/*
* Parse an HTTP response body from a buffer of size "sz".
* Returns an opaque pointer on success, otherwise NULL on error.
*/
struct jsmnn *
json_parse(const char *buf, size_t sz)
{
struct jsmnn *n;
jsmn_parser p;
jsmntok_t *tok;
int r;
size_t tokcount;
jsmn_init(&p);
tokcount = 128;
/* Do this until we don't need any more tokens. */
again:
tok = calloc(tokcount, sizeof(jsmntok_t));
if (tok == NULL) {
warn("calloc");
return NULL;
}
/* Actually try to parse the JSON into the tokens. */
r = jsmn_parse(&p, buf, sz, tok, tokcount);
if (r < 0 && r == JSMN_ERROR_NOMEM) {
tokcount *= 2;
free(tok);
goto again;
} else if (r < 0) {
warnx("jsmn_parse: %d", r);
free(tok);
return NULL;
}
/* Now parse the tokens into a tree. */
n = jsmntree_alloc(tok, buf, r);
free(tok);
return n;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2017 Reyk Floeter <reyk@openbsd.org>
* Copyright (c) 2017, 2018, 2019 Reyk Floeter <reyk@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@ -15,32 +15,48 @@
*/
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/if_ether.h>
#include <limits.h>
#include <stdio.h>
#include <syslog.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <resolv.h>
#include <netdb.h>
#include <ctype.h>
#include <fcntl.h>
#include <errno.h>
#include <poll.h>
#include <pwd.h>
#include <err.h>
#include "main.h"
#include "xml.h"
__dead void usage(void);
static struct system_config *agent_init(const char *, int, int);
static int agent_configure(struct system_config *);
static void agent_free(struct system_config *);
static int agent_pf(struct system_config *, int);
static void agent_unconfigure(void);
static char *metadata_parse(char *, size_t, enum strtype);
static int agent_timeout;
__dead void usage(void);
static struct system_config
*agent_init(const char *, 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(struct system_config *,
const unsigned char *, size_t);
static void agent_unconfigure(void);
static char *metadata_parse(char *, size_t, enum strtype);
static int agent_timeout;
static char *cloudnames[] = CLOUDNAMES;
int
shell(const char *arg, ...)
@ -247,7 +263,7 @@ enable_output(struct system_config *sc, int fd, int oldfd)
}
char *
get_string(u_int8_t *ptr, size_t len)
get_string(const unsigned char *ptr, size_t len)
{
size_t i;
@ -265,7 +281,7 @@ get_string(u_int8_t *ptr, size_t len)
}
char *
get_line(u_int8_t *ptr, size_t len)
get_line(const unsigned char *ptr, size_t len)
{
size_t i;
@ -280,7 +296,7 @@ get_line(u_int8_t *ptr, size_t len)
}
char *
get_word(u_int8_t *ptr, size_t len)
get_word(const unsigned char *ptr, size_t len)
{
size_t i;
@ -294,26 +310,34 @@ get_word(u_int8_t *ptr, size_t len)
}
static struct system_config *
agent_init(const char *ifname, int dryrun, int timeout)
agent_init(const char *ifname, const char *rootdisk, 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;
sc->sc_rootdisk = rootdisk;
TAILQ_INIT(&sc->sc_pubkeys);
TAILQ_INIT(&sc->sc_netaddrs);
if ((sc->sc_nullfd = open("/dev/null", O_RDWR)) == -1) {
free(sc);
return (NULL);
}
if ((sc->sc_username = strdup("puffy")) == NULL) {
free(sc);
close(sc->sc_nullfd);
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)
@ -331,13 +355,26 @@ 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);
}
if (sc->sc_password_plain != NULL) {
/* XXX can be removed with calloc_conceal() post-6.6 */
explicit_bzero(sc->sc_password_plain,
strlen(sc->sc_password_plain));
free(sc->sc_password_plain);
}
free(sc->sc_password_hash);
free(sc->sc_hostname);
free(sc->sc_username);
free(sc->sc_password);
free(sc->sc_userdata);
free(sc->sc_endpoint);
free(sc->sc_instance);
free(sc->sc_args);
close(sc->sc_nullfd);
while ((ssh = TAILQ_FIRST(&sc->sc_pubkeys))) {
@ -346,6 +383,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
@ -399,6 +442,114 @@ 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 (na->net_type == NET_DNS_DOMAIN &&
strcasecmp(na->net_value, net->net_value) != 0)
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_DNS_DOMAIN:
if (strlen(value) >= NI_MAXHOST) {
log_debug("%s: if%u domain %s", __func__, unit, value);
free(net);
return (-1);
}
break;
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;
}
if ((net->net_value = strdup(value)) == NULL) {
free(net);
return (-1);
}
/* Address already exists, ignore new entry */
if ((na = agent_getnetaddr(sc, net)) != NULL) {
free(net->net_value);
free(net);
return (0);
}
TAILQ_INSERT_TAIL(&sc->sc_netaddrs, net, net_entry);
return (0);
}
static int
fileout(const char *str, const char *mode, const char *fmt, ...)
{
@ -474,7 +625,7 @@ agent_pf(struct system_config *sc, int open)
return (0);
if (open)
ret = shellout("pass out proto tcp from egress to port www\n",
ret = shellout("pass out proto tcp from (egress) to port www\n",
NULL, "pfctl", "-f", "-", NULL);
else
ret = shellout("\n", NULL, "pfctl", "-f", "-", NULL);
@ -485,6 +636,7 @@ agent_pf(struct system_config *sc, int open)
static int
agent_configure(struct system_config *sc)
{
char pwbuf[_PASSWORD_LEN + 2];
struct ssh_pubkey *ssh;
char *str1, *str2;
@ -501,6 +653,13 @@ agent_configure(struct system_config *sc)
if (fileout(sc->sc_instance, "w", "/var/db/cloud-instance") != 0)
log_warnx("instance failed");
/* Set default username if not set */
if ((sc->sc_username == NULL) &&
(sc->sc_username = strdup("puffy")) == NULL) {
log_warn("default username");
return (-1);
}
/* hostname */
log_debug("%s: hostname %s", __func__, sc->sc_hostname);
if (fileout(sc->sc_hostname, "w", "/etc/myname") != 0)
@ -510,38 +669,49 @@ agent_configure(struct system_config *sc)
/* username */
log_debug("%s: username %s", __func__, sc->sc_username);
if (shell("useradd", "-L", "staff", "-G", "wheel",
"-m", sc->sc_username, NULL) != 0)
log_warnx("username failed");
if (fileout(sc->sc_username, "w", "/root/.forward") != 0)
log_warnx(".forward failed");
if (strcmp("root", sc->sc_username) != 0) {
if (shell("useradd", "-L", "staff", "-G", "wheel",
"-m", sc->sc_username, NULL) != 0)
log_warnx("username failed");
if (fileout(sc->sc_username, "w", "/root/.forward") != 0)
log_warnx(".forward failed");
}
/* password */
if (sc->sc_password == NULL) {
str1 = "/PasswordAuthentication/"
"s/.*/PasswordAuthentication no/";
if (sc->sc_password_hash == NULL) {
if (asprintf(&str2, "permit keepenv nopass %s as root\n"
"permit keepenv nopass root\n", sc->sc_username) == -1)
str2 = NULL;
} else {
if (shell("usermod", "-p", sc->sc_password,
if (shell("usermod", "-p", sc->sc_password_hash,
sc->sc_username, NULL) != 0)
log_warnx("password failed");
str1 = "/PasswordAuthentication/"
"s/.*/PasswordAuthentication yes/";
if (asprintf(&str2, "permit keepenv persist %s as root\n"
"permit keepenv nopass root\n", sc->sc_username) == -1)
str2 = NULL;
/* write generated password as comment to authorized_keys */
if (sc->sc_password_plain != NULL) {
snprintf(pwbuf, sizeof(pwbuf), "# %s",
sc->sc_password_plain);
if (fileout(pwbuf, "w",
"%s/%s/.ssh/authorized_keys",
strcmp("root", sc->sc_username) == 0 ? "" : "/home",
sc->sc_username) != 0)
log_warnx("password comment failed");
explicit_bzero(pwbuf, sizeof(pwbuf));
}
}
/* doas */
if (str2 == NULL || fileout(str2, "w", "/etc/doas.conf") != 0)
if ((strcmp("root", sc->sc_username) != 0) &&
(str2 == NULL || fileout(str2, "w", "/etc/doas.conf")) != 0)
log_warnx("doas failed");
free(str2);
/* ssh configuration */
if (sc->sc_password == NULL && !TAILQ_EMPTY(&sc->sc_pubkeys))
if (sc->sc_password_hash == NULL && !TAILQ_EMPTY(&sc->sc_pubkeys))
str1 = "/PasswordAuthentication/"
"s/.*/PasswordAuthentication no/";
else
@ -558,15 +728,21 @@ agent_configure(struct system_config *sc)
continue;
log_debug("%s: key %s", __func__, ssh->ssh_keyval);
if (fileout(ssh->ssh_keyval, "a",
"/home/%s/.ssh/authorized_keys",
"%s/%s/.ssh/authorized_keys",
strcmp("root", sc->sc_username) == 0 ? "" : "/home",
sc->sc_username) != 0)
log_warnx("public key failed");
}
if (sc->sc_userdata) {
/* XXX */
if (agent_userdata(sc, sc->sc_userdata,
strlen(sc->sc_userdata)) != 0)
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 <<EOF\n"
"#############################################################\n"
@ -582,6 +758,164 @@ agent_configure(struct system_config *sc)
return (0);
}
static int
agent_userdata(struct system_config *sc,
const unsigned char *userdata, size_t len)
{
char *shebang = NULL, *str = NULL, *line = NULL;
const char *file;
int ret = -1;
if (len <= 2) {
log_warnx("user-data too short");
goto fail;
}
if (userdata[0] == 0x1f && userdata[1] == 0x8b) {
log_warnx("gzip-compressed user-data is not supported");
goto fail;
} else if (userdata[0] == '#') {
if ((shebang = get_line(userdata, len)) == NULL) {
log_warnx("failed to decode shebang from user-data");
goto fail;
}
} else if (isprint(userdata[0]) && isprint(userdata[1])) {
/* Decode user-data and call the function again */
if ((str = calloc(1, len + 1)) == NULL ||
(len = b64_pton(userdata, str, len)) < 1 ||
agent_userdata(sc, str, len) != 0) {
log_warnx("failed to decode user-data");
goto fail;
}
goto done;
}
log_debug("%s: user-data: %s", __func__, shebang);
if (strlen(shebang) <= 2 || strncmp("#!", shebang, 2) != 0) {
log_warnx("unsupported user-data type");
goto fail;
}
/* now get the whole script */
if ((str = get_string(userdata, len)) == NULL) {
log_warnx("invalid user-data script");
goto fail;
}
if (sc->sc_dryrun)
goto done;
/* write user-data script into file */
file = "/etc/rc.user-data";
if (fileout(str, "w", file) != 0) {
log_warnx("failed to write user-data");
goto fail;
}
/* and call it from rc.firsttime later on boot */
if (asprintf(&line,
"logger -s -t cloud-agent \"running user-data\"\n"
"%s %s\nrm %s\n", shebang + 2, file, file) == -1 ||
fileout(line, "a", "/etc/rc.firsttime") != 0)
log_warnx("failed to add user-data script");
done:
ret = 0;
fail:
free(line);
free(str);
free(shebang);
return (ret);
}
static int
agent_network(struct system_config *sc)
{
struct net_addr *net;
char ift[16], ifname[16], line[1024];
const char *family;
char domain[(NI_MAXHOST + 1) * 6 + 8]; /* up to 6 domains */
int has_domain = 0;
char ifidx[UINT16_MAX], *str;
const char *comment = "# Generated by cloud-agent";
if (!sc->sc_network)
return (0);
if (strlcpy(ift, sc->sc_interface, sizeof(ift)) >= sizeof(ift))
return (-1);
ift[strcspn(ift, "0123456789")] = '\0';
memset(ifidx, 0, sizeof(ifidx));
snprintf(domain, sizeof(domain), "search ");
fileout(comment, "w", "/etc/mygate");
fileout(comment, "w", "/etc/resolv.conf");
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 or mask */
/* hostname.if startup configuration */
if (!ifidx[net->net_ifunit])
fileout(comment, "w",
"/etc/hostname.%s", ifname);
snprintf(line, sizeof(line), "%s alias %s",
family, net->net_value);
fileout(line, "a", "/etc/hostname.%s", ifname);
if (!ifidx[net->net_ifunit]++ &&
net->net_ifunit == 0) {
snprintf(line, sizeof(line),
"!%s", sc->sc_args);
fileout(line, "a", "/etc/hostname.%s", ifname);
}
/* 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;
case NET_DNS_DOMAIN:
if (!has_domain++) {
/* use the first search domain as our own */
snprintf(line, sizeof(line), "domain %s",
net->net_value);
fileout(line, "a", "/etc/resolv.conf");
} else
(void)strlcat(domain, " ", sizeof(domain));
(void)strlcat(domain, net->net_value, sizeof(domain));
break;
default:
break;
}
}
if (has_domain)
fileout(domain, "a", "/etc/resolv.conf");
/* append resolv.conf.tail if it exists */
if ((str = filein("r", "/etc/resolv.conf.tail")) != NULL) {
fileout(str, "a", "/etc/resolv.conf");
free(str);
}
return (0);
}
void
agent_unconfigure(void)
{
@ -608,8 +942,10 @@ agent_unconfigure(void)
(void)fileout("permit keepenv persist :wheel as root\n"
"permit keepenv nopass root\n", "w", "/etc/doas.conf");
}
/* Remove cloud-instance file */
(void)unlink("/var/db/cloud-instance");
}
static char *
metadata_parse(char *s, size_t sz, enum strtype type)
@ -638,10 +974,12 @@ metadata(struct system_config *sc, const char *path, enum strtype type)
struct httpget *g = NULL;
char *str = NULL;
log_debug("%s: %s", __func__, path);
g = http_get(&sc->sc_addr, 1,
sc->sc_endpoint, 80, path, NULL, 0, NULL);
if (g != NULL)
log_debug("%s: HTTP %d http://%s%s", __func__, g->code,
sc->sc_endpoint, path);
if (g != NULL && g->code == 200 && g->bodypartsz > 0)
str = metadata_parse(g->bodypart, g->bodypartsz, type);
http_get_free(g);
@ -778,7 +1116,8 @@ dhcp_getendpoint(struct system_config *sc)
sc->sc_addr.ip = sc->sc_endpoint;
sc->sc_addr.family = 4;
log_debug("%s: %s", __func__, ep);
if (log_getverbose() > 2)
log_debug("%s: %s", __func__, ep);
return (0);
}
@ -788,35 +1127,217 @@ usage(void)
{
extern char *__progname;
fprintf(stderr, "usage: %s [-nuv] [-t 3] interface\n",
__progname);
fprintf(stderr, "usage: %s [-nuv] "
"[-c cloud[,...]] [-p length] [-r rootdisk]\n\t"
"[-t 3] [-U puffy] interface\n", __progname);
exit(1);
}
static char *
get_args(int argc, char *const *argv)
{
char *args, path[PATH_MAX];
size_t argslen = 0;
int i;
/* Store args in a string */
for (i = 0; i < argc; i++) {
if (i == 0) {
realpath(argv[0], path);
argslen += strlen(path) + 1;
} else {
argslen += strlen(argv[i]) + 1;
}
}
if ((args = calloc(1, argslen + 1)) == NULL)
return (NULL);
for (i = 0; i < argc; i++) {
if (i == 0)
strlcat(args, path, argslen);
else {
strlcat(args, " ", argslen);
strlcat(args, argv[i], argslen);
}
}
return (args);
}
static char *
pwgen(size_t len, char **hash)
{
char *password;
size_t i, alphabet_len;
const char *alphabet =
"0123456789_"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
alphabet_len = strlen(alphabet);
*hash = NULL;
/* XXX use calloc_conceal() post-6.6 */
if ((password = calloc(1, len + 1)) == NULL)
return (NULL);
/* Simple password generator */
for (i = 0; i < len; i++)
password[i] = alphabet[arc4random_uniform(alphabet_len)];
if ((*hash = calloc(1, _PASSWORD_LEN)) == NULL) {
freezero(password, len + 1);
return (NULL);
}
if (crypt_newhash(password, "bcrypt,a",
*hash, _PASSWORD_LEN) != 0) {
freezero(password, len + 1);
freezero(*hash, _PASSWORD_LEN);
password = NULL;
*hash = NULL;
}
return (password);
}
static void
cloud_add(enum cloudname name, struct clouds *clouds)
{
struct cloud *cloud;
TAILQ_FOREACH(cloud, clouds, cloud_entry) {
if (cloud->cloud_name == name)
fatalx("cloud %s defined twice",
cloudnames[name]);
}
if ((cloud = calloc(1, sizeof(*cloud))) == NULL)
fatal("%s: calloc", __func__);
cloud->cloud_name = name;
switch (name) {
case AZURE:
cloud->fetch = azure;
break;
case CLOUDINIT:
cloud->fetch = cloudinit;
break;
case EC2:
cloud->fetch = ec2;
break;
case OPENNEBULA:
cloud->fetch = opennebula;
break;
case OPENSTACK:
cloud->fetch = openstack;
break;
}
TAILQ_INSERT_TAIL(clouds, cloud, cloud_entry);
}
static int
trycloud(struct system_config *sc, struct cloud *cloud)
{
int errfd = -1, ret = -1;
free(sc->sc_endpoint);
sc->sc_endpoint = NULL;
switch (sc->sc_state) {
case STATE_INIT:
if ((cloud = TAILQ_FIRST(sc->sc_clouds)) == NULL)
return (0);
sc->sc_stack = cloudnames[cloud->cloud_name];
log_debug("%s: %s", __func__, sc->sc_stack);
TAILQ_REMOVE(sc->sc_clouds, cloud, cloud_entry);
break;
case STATE_DHCP:
sc->sc_state = STATE_169;
if (dhcp_getendpoint(sc) == -1)
return trycloud(sc, cloud);
break;
case STATE_169:
sc->sc_state = STATE_DONE;
if ((sc->sc_endpoint = strdup(DEFAULT_ENDPOINT)) == NULL) {
log_warnx("failed to set defaults");
goto done;
}
break;
case STATE_DONE:
sc->sc_state = STATE_INIT;
ret = trycloud(sc, NULL);
goto done;
}
errfd = disable_output(sc, STDERR_FILENO);
ret = (*cloud->fetch)(sc);
enable_output(sc, STDERR_FILENO, errfd);
if (ret != 0)
return trycloud(sc, cloud);
done:
free(cloud);
return (0);
}
int
main(int argc, char *const *argv)
{
struct system_config *sc;
int verbose = 0, dryrun = 0, unconfigure = 0;
int ch, ret, timeout = CONNECT_TIMEOUT;
const char *error = NULL;
int verbose = 0, dryrun = 0, unconfigure = 0, sub;
int genpw = 0, ch, ret, timeout = CONNECT_TIMEOUT;
const char *error = NULL, *rootdisk = NULL;
char *args, *username = NULL, *options, *value;
struct clouds clouds;
while ((ch = getopt(argc, argv, "nvt:u")) != -1) {
/* log to stderr */
log_init(1, LOG_DAEMON);
if ((args = get_args(argc, argv)) == NULL)
fatalx("failed to save args");
TAILQ_INIT(&clouds);
while ((ch = getopt(argc, argv, "c:np:r:t:U:uv")) != -1) {
switch (ch) {
case 'c':
options = optarg;
while (*options) {
if ((sub = getsubopt(&options,
cloudnames, &value)) == -1)
fatalx("invalid cloud stack");
else if (value != NULL)
fatalx("unexpected value");
cloud_add(sub, &clouds);
}
break;
case 'n':
dryrun = 1;
break;
case 'v':
verbose += 2;
case 'p':
genpw = strtonum(optarg, 8, 8192, &error);
if (error != NULL)
fatalx("invalid password length: %s", error);
break;
case 'r':
rootdisk = optarg;
break;
case 't':
timeout = strtonum(optarg, -1, 86400, &error);
if (error != NULL)
fatalx("invalid timeout: %s", error);
break;
case 'U':
if ((username = strdup(optarg)) == NULL)
fatal("username");
break;
case 'u':
unconfigure = 1;
break;
case 'v':
verbose += 2;
break;
default:
usage();
}
@ -825,8 +1346,6 @@ main(int argc, char *const *argv)
argv += optind;
argc -= optind;
/* log to stderr */
log_init(1, LOG_DAEMON);
log_setverbose(verbose);
if (unconfigure) {
@ -837,22 +1356,65 @@ main(int argc, char *const *argv)
if (argc != 1)
usage();
if (pledge("stdio cpath rpath wpath exec proc dns inet", NULL) == -1)
if (pledge(rootdisk == NULL ?
"stdio cpath rpath wpath exec proc dns inet" :
"stdio cpath rpath wpath exec proc dns inet disklabel",
NULL) == -1)
fatal("pledge");
if ((sc = agent_init(argv[0], dryrun, timeout)) == NULL)
if ((sc = agent_init(argv[0], rootdisk, dryrun, timeout)) == NULL)
fatalx("agent");
/*
* XXX Detect cloud with help from hostctl and sysctl
* XXX in addition to the interface name.
*/
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 (rootdisk != NULL && growdisk(sc) == -1)
fatalx("failed to grow %s", rootdisk);
sc->sc_clouds = &clouds;
sc->sc_args = args;
if (username != NULL) {
free(sc->sc_username);
sc->sc_username = username;
}
if (TAILQ_EMPTY(&clouds)) {
/*
* XXX Auto-detect cloud with help from hostctl and
* XXX sysctl in addition to the interface name.
*/
if (strcmp("hvn0", sc->sc_interface) == 0) {
cloud_add(AZURE, &clouds);
} else if (strcmp("xnf0", sc->sc_interface) == 0) {
cloud_add(OPENNEBULA, &clouds);
cloud_add(EC2, &clouds);
} else {
cloud_add(OPENNEBULA, &clouds);
cloud_add(OPENSTACK, &clouds);
cloud_add(CLOUDINIT, &clouds);
}
}
ret = trycloud(sc, NULL);
if (genpw) {
if (sc->sc_password_hash != NULL)
log_debug("%s: user password hash: %s", __func__,
sc->sc_password_hash);
else if ((sc->sc_password_plain = pwgen(genpw,
&sc->sc_password_hash)) != NULL) {
if (log_getverbose() > 2)
log_debug("%s: generated password: %s",
__func__, sc->sc_password_plain);
} else
log_warnx("failed to generate password");
}
/* Debug userdata */
if (sc->sc_dryrun && sc->sc_userdata) {
if (agent_userdata(sc, sc->sc_userdata,
strlen(sc->sc_userdata)) != 0)
log_warnx("user-data failed");
}
if (sc->sc_stack != NULL)
log_debug("%s: %s", __func__, sc->sc_stack);
if (sc->sc_dryrun) {
agent_free(sc);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2017 Reyk Floeter <reyk@openbsd.org>
* Copyright (c) 2017, 2018, 2019 Reyk Floeter <reyk@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@ -29,12 +29,30 @@
#define DEFAULT_ENDPOINT "169.254.169.254"
#define CONNECT_TIMEOUT 10 /* in seconds */
enum cloudname {
AZURE,
CLOUDINIT,
EC2,
OPENNEBULA,
OPENSTACK
};
#define CLOUDNAMES { \
"azure", "cloudinit", "ec2", "opennebula", "openstack", NULL \
}
enum strtype {
WORD,
LINE,
TEXT
};
enum state {
STATE_INIT,
STATE_DHCP,
STATE_169,
STATE_DONE
};
struct ssh_pubkey {
char *ssh_keyval;
char *ssh_keyfp;
@ -43,23 +61,66 @@ 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_DNS_DOMAIN,
NET_MAX
};
struct net_addr {
enum net_type net_type;
unsigned short 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;
struct cloud {
enum cloudname cloud_name;
int (*fetch)(struct system_config *);
TAILQ_ENTRY(cloud) cloud_entry;
};
TAILQ_HEAD(clouds, cloud);
struct system_config {
const char *sc_stack;
char *sc_args;
char *sc_hostname;
char *sc_username;
char *sc_password;
char *sc_password_plain;
char *sc_password_hash;
char *sc_pubkey;
char *sc_userdata;
char *sc_endpoint;
enum state sc_state;
struct clouds *sc_clouds;
char *sc_instance;
int sc_timeout;
const char *sc_ovfenv;
const char *sc_interface;
const char *sc_cdrom;
const char *sc_rootdisk;
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,19 +153,29 @@ 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 *);
/* growdisk.c */
int growdisk(struct system_config *);
/* main.c */
int shell(const char *, ...);
int shellout(const char *, char **, const char *, ...);
int disable_output(struct system_config *, int);
int enable_output(struct system_config *, int, int);
char *get_string(u_int8_t *, size_t);
char *get_line(u_int8_t *, size_t);
char *get_word(u_int8_t *, size_t);
char *get_string(const unsigned char *, size_t);
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);

291
agent/opennebula.c Normal file
View File

@ -0,0 +1,291 @@
/*
* Copyright (c) 2018, 2019 Reyk Floeter <reyk@openbsd.org>
*
* 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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fnmatch.h>
#include <fcntl.h>
#include <ctype.h>
#include <sha2.h>
#include <err.h>
#include <util.h>
#include "main.h"
int
opennebula(struct system_config *sc)
{
FILE *fp;
const char *delim = "\\\\\0", *errstr;
char *line = NULL, *k, *v, *p, q;
char *value = NULL, *next = NULL, *last;
char *hname = NULL, *uname = NULL;
size_t len, lineno = 0, i;
int ret = -1;
unsigned short unit;
if (sc->sc_state == STATE_INIT) {
sc->sc_state = STATE_169;
return (-1);
}
/* Return silently without error */
if ((fp = fopen("/mnt/context.sh", "r")) == NULL)
goto done;
while ((line = fparseln(fp, &len, &lineno,
delim, FPARSELN_UNESCALL)) != NULL) {
/* key */
k = line + strspn(line, " \t\r");
/* a context always starts with this header */
if (lineno == 1) {
ret = strcmp(k,
"# Context variables generated by OpenNebula");
if (ret != 0) {
log_debug("%s: unsupported context", __func__);
goto done;
}
free(line);
continue;
}
/* Strip comments that do not occur within a value */
if (*k == '#') {
free(line);
continue;
}
/* value */
if ((v = strchr(line, '=')) == NULL || *(v + 1) == '\0') {
free(line);
continue;
}
*v++ = '\0';
/* value is quoted */
q = *v;
if (strspn(v, "\"'") == 0) {
free(line);
continue;
}
*v++ = '\0';
/* quoted value can be continued on multiple lines */
if ((value = strdup("")) == NULL) {
log_debug("%s: strdup", __func__);
goto done;
}
next = v;
do {
if ((p = strrchr(next, q)) != NULL)
*p++ = '\0';
if (*next) {
last = value;
if (asprintf(&value, "%s%s\n",
last, next) == -1) {
log_debug("%s: asprintf", __func__);
if (next != v)
free(next);
goto done;
}
free(last);
}
if (next != v)
free(next);
} while (p == NULL &&
(next = fparseln(fp, &len, &lineno,
delim, FPARSELN_UNESCALL)) != NULL);
next = NULL;
v = value;
/* strip trailing newline */
if ((p = strrchr(v, '\n')) != NULL)
*p = '\0';
/* continue if value is empty */
if (*v == '\0') {
free(line);
free(value);
value = NULL;
continue;
}
/* print key/value unless it is a multi-line value */
if (strcasecmp("SSH_PUBLIC_KEY", k) != 0 &&
strcasecmp("START_SCRIPT", k) != 0 &&
strcasecmp("START_SCRIPT_BASE64", k) != 0)
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, UINT16_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("SEARCH_DOMAIN", k) == 0) {
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_DOMAIN)) != 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("HOSTNAME", k) == 0) {
if ((hname = strdup(v)) == NULL)
log_warnx("failed to set hostname");
} else if (strcasecmp("SSH_PUBLIC_KEY", k) == 0) {
do {
p = v + strcspn(v, "\n");
*p++ = '\0';
if (*v)
log_debug("%s: %s = %s",
__func__, k, v);
if (*v && agent_addpubkey(sc, v, NULL) != 0)
log_warnx("failed to set ssh pubkey");
v = p + strspn(p, "\n");
} while (*v != '\0');
} else if (strcasecmp("START_SCRIPT", k) == 0 ||
strcasecmp("START_SCRIPT_BASE64", k) == 0) {
log_debug("%s: %s = ...", __func__, k);
/* We will detect and decode base64 later */
if ((sc->sc_userdata = strdup(v)) == NULL)
log_warnx("failed to set userdata");
} else if (strcasecmp("USERNAME", k) == 0) {
if ((uname = strdup(v)) == NULL)
log_warnx("failed to set username");
else {
free(sc->sc_username);
sc->sc_username = uname;
}
}
free(line);
free(value);
value = NULL;
}
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);
free(value);
return (ret);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018 Reyk Floeter <reyk@openbsd.org>
* Copyright (c) 2018, 2019 Reyk Floeter <reyk@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@ -34,17 +34,11 @@ static int openstack_fetch(struct system_config *);
int
openstack(struct system_config *sc)
{
if ((dhcp_getendpoint(sc) == -1) &&
(sc->sc_endpoint = strdup(DEFAULT_ENDPOINT)) == NULL) {
log_warnx("failed to set defaults");
if (sc->sc_state == STATE_INIT) {
sc->sc_state = STATE_DHCP;
return (-1);
}
if (openstack_fetch(sc) != 0) {
free(sc->sc_endpoint);
return (cloudinit(sc));
}
return (0);
return openstack_fetch(sc);
}
static int

150
cloud-agent.md Normal file
View File

@ -0,0 +1,150 @@
CLOUD-AGENT(8) - System Manager's Manual
# NAME
**cloud-agent** - cloud provisioning for OpenBSD VMs
# SYNOPSIS
**cloud-agent**
\[**-nuv**]
\[**-c**&nbsp;*cloud*\[,*cloud*...]]
\[**-p**&nbsp;*length*]
\[**-r**&nbsp;*rootdisk*]
\[**-t**&nbsp;*timeout*]
\[**-U**&nbsp;*username*]
*interface*
# DESCRIPTION
The
**cloud-agent**
program manages the OpenBSD provisioning and VM interaction in cloud
environments.
The options are as follows:
**-c** *cloud*\[,*cloud*...]
> Probe a list of cloud stacks for provisioning in the specified order.
> If this option is not specified,
> **cloud-agent**
> tries to detect the environment and possible cloud stacks automatically.
> Supported
> *cloud*
> stacks are:
> **azure**
> > Microsoft Azure
> **cloudinit**
> > Generic cloud-init
> **ec2**
> > Amazon AWS EC2
> **opennebula**
> > OpenNebula
> **openstack**
> > OpenStack
**-p** *length*
> Generate and set a random password for the default user.
> The password will be written in its plain form into the
> *~/.ssh/authorized\_keys*
> file.
> This allows to use the
> doas(1)
> command to gain root privileges.
> The minimum
> *length*
> is 8 characters and the default is an empty password.
**-n**
> Do not configure the system and skip the provisioning step.
**-t** *timeout*
> Change the HTTP timeout.
> The default is 3 seconds.
**-U** *username*
> Change the default user.
> The default is
> "ec2-user"
> on AWS,
> "azure-user"
> on Azure, and
> "puffy"
> everywhere else.
> The default user is used when it is not obtained from the cloud
> configuration.
> Using
> "root"
> is supported, but not recommended.
**-r** *rootdisk*
> Automatically grow the last
> OpenBSD
> FFS partition of the root disk to use all the available space.
**-u**
> Deprovision and unconfigure the system.
> This deletes keys, passwords, and logs files without asking for permission.
**-v**
> Produce more verbose output.
Enable
**cloud-agent**
in the
hostname.if(5)
of the VM's primary networking interface and automatically the last
partition of the root disk:
# cat /etc/hostname.hvn0
dhcp
!/usr/local/libexec/cloud-agent -r sd0 "\$if"
# FILES
*~/.ssh/authorized\_keys*
> The location of the agent-configured SSH public keys and optional password.
*/usr/local/libexec/cloud-agent*
> The agent itself.
*/usr/local/bin/cms*
> The CMS binary that is used to decrypt messages from the Azure fabric.
*/var/db/cloud-instance*
> The instance ID as reported by the cloud.
> **cloud-agent**
> reprovisions the system when the value has changed.
# SEE ALSO
meta-data(8),
vmd(8)
# AUTHORS
Reyk Floeter &lt;[reyk@openbsd.org](mailto:reyk@openbsd.org)&gt;
OpenBSD 6.5 - June 26, 2019

View File

@ -80,6 +80,13 @@ DECLARE_ASN1_FUNCTIONS(CMS_ContentInfo)
DECLARE_ASN1_FUNCTIONS(CMS_ReceiptRequest)
DECLARE_ASN1_PRINT_FUNCTION(CMS_ContentInfo)
#if defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER >= 0x20900000L
#define M_ASN1_new_of(type) \
(type *)ASN1_item_new(ASN1_ITEM_rptr(type))
#define M_ASN1_free_of(x, type) \
ASN1_item_free(CHECKED_PTR_OF(type, x), ASN1_ITEM_rptr(type))
#endif
# define CMS_SIGNERINFO_ISSUER_SERIAL 0
# define CMS_SIGNERINFO_KEYIDENTIFIER 1

View File

@ -353,7 +353,11 @@ void cms_DigestAlgorithm_set(X509_ALGOR *alg, const EVP_MD *md)
BIO *cms_DigestAlgorithm_init_bio(X509_ALGOR *digestAlgorithm)
{
BIO *mdbio = NULL;
#if LIBRESSL_VERSION_NUMBER >= 0x2080000fL
const ASN1_OBJECT *digestoid;
#else
ASN1_OBJECT *digestoid;
#endif
const EVP_MD *digest;
X509_ALGOR_get0(&digestoid, NULL, NULL, digestAlgorithm);
digest = EVP_get_digestbyobj(digestoid);
@ -380,7 +384,11 @@ int cms_DigestAlgorithm_find_ctx(EVP_MD_CTX *mctx, BIO *chain,
X509_ALGOR *mdalg)
{
int nid;
#if LIBRESSL_VERSION_NUMBER >= 0x2080000fL
const ASN1_OBJECT *mdoid;
#else
ASN1_OBJECT *mdoid;
#endif
X509_ALGOR_get0(&mdoid, NULL, NULL, mdalg);
nid = OBJ_obj2nid(mdoid);
/* Look for digest type to match signature */

View File

@ -328,7 +328,11 @@ CMS_SignerInfo *CMS_add1_signer(CMS_ContentInfo *cms,
/* See if digest is present in digestAlgorithms */
for (i = 0; i < sk_X509_ALGOR_num(sd->digestAlgorithms); i++) {
#if LIBRESSL_VERSION_NUMBER >= 0x2080000fL
const ASN1_OBJECT *aoid;
#else
ASN1_OBJECT *aoid;
#endif
alg = sk_X509_ALGOR_value(sd->digestAlgorithms, i);
X509_ALGOR_get0(&aoid, NULL, NULL, alg);
if (OBJ_obj2nid(aoid) == EVP_MD_type(md))