/*
 * nfsroot-generator:
 *   systemd generator to mount /sysroot via NFSv4 in the initrd
 */

#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "nfslib.h"
#include "systemd.h"

struct nfsroot_info {
	char	*server;
	char	*path;
	char	*options;
};

/* Valid kernel command line options:
 *
 * - root=/dev/nfs nfsroot=[<server>:]<path>[,<options>]
 *
 *   Defined in <kernel_source>/Documentation/admin-guide/nfs/nfsroot.rst
 *
 * - root=nfs[4]:[<server>:]<path>[:<options>]
 *
 *   Defined in dracut.cmdline(7).
 *
 * Allow to use both "nfs" or "nfs4" as type indicator,
 * although the mount unit will use always "nfs4".
 *
 * If <server> is not set, it is determined by the 2nd
 * token of the ip parameter:
 *
 *     ip=<client-ip>:<server-ip>:...
 *
 * <options> can be appended with the prefix ":" or ","
 * and are separated by ",".
 */
static int get_nfsroot_info_from_cmdline(struct nfsroot_info *info)
{
	FILE	*f;
	char	buf[4096], *p;
	char	*root = NULL, *nfsroot = NULL, *ip = NULL;
	char	*colon, *slash, *comma;
	size_t	size;

	f = fopen("/proc/cmdline", "r");
	if (!f)
		return errno;

	if (fgets(buf, sizeof(buf), f) == NULL)
		return EIO;

	fclose(f);

	size = strlen(buf);
	if (buf[size - 1] == '\n')
		buf[size - 1] = '\0';
	for (p = strtok(buf, " "); p; p = strtok(NULL, " ")) {
		if (!strncmp(p, "root=", strlen("root=")))
			root = p + strlen("root=");
		else if (!strncmp(p, "nfsroot=", strlen("nfsroot=")))
			nfsroot = p + strlen("nfsroot=");
		else if (!strncmp(p, "ip=", strlen("ip=")))
			ip = p + strlen("ip=");
	}

	if (!strcmp(root, "/dev/nfs")) {
		/* Require the nfsroot parameter with the pseudo-NFS-device */
		if (!nfsroot)
			return EINVAL;

	} else if (root) {
		/* Mount type: "nfs" or "nfs4" */
		colon = strchr(root, ':');
		if (colon == NULL)
			return 0;
		if (strncmp(root, "nfs:", strlen("nfs:")) &&
			strncmp(root, "nfs4:", strlen("nfs4:")))
			return 0;

		nfsroot = colon + 1;
	}

	slash = strchr(nfsroot, '/');
	if (slash == NULL)
		return EINVAL;

	if (slash - nfsroot > 0) {
		if ((slash - 1)[0] != ':')
			return EINVAL;

		info->server = strndup(nfsroot, slash - nfsroot - 1);
		nfsroot = slash;
	} else {
		/* Require the ip parameter if <server> is unset */
		if (!ip)
			return EINVAL;

		colon = strchr(ip, ':');
		if (colon == NULL)
			return EINVAL;
		ip = colon + 1;
		colon = strchr(ip, ':');
		if (colon == NULL)
			return EINVAL;
		info->server = strndup(ip, colon - ip);
	}
	if (!info->server)
		return ENOMEM;

	colon = strchr(nfsroot, ':');
	if (colon != NULL) {
		info->path = strndup(nfsroot, colon - nfsroot);
		nfsroot = colon + 1;
	} else {
		comma = strchr(nfsroot, ',');
		if (comma == NULL) {
			info->path = strdup(nfsroot);
		} else {
			info->path = strndup(nfsroot, comma - nfsroot);
			nfsroot = comma + 1;
		}
	}
	if (!info->path)
		return ENOMEM;

	if ((colon || comma) && strlen(nfsroot) > 0) {
		/* Skip checking <options> format */
		info->options = strdup(nfsroot);
		if (!info->options)
			return ENOMEM;
	}

	return 0;
}

static int generate_sysroot_mount_unit(const struct nfsroot_info *info,
			       const char *dirname)
{
	int 	ret = 0;
	char	*path, *rpath;
	char	dirbase[] = "/initrd-root-fs.target.requires";
	char	filebase[] = "/sysroot.mount";
	FILE	*f;

	path = malloc(strlen(dirname) + 1 + sizeof(filebase));
	if (!path)
		return ENOMEM;
	sprintf(path, "%s", dirname);
	mkdir(path, 0755);
	strcat(path, filebase);
	f = fopen(path, "w");
	if (!f) {
		ret = errno;
		goto cleanup;
	}

	fprintf(f, "# Automatically generated by nfsroot-generator\n\n[Unit]\n");
	fprintf(f, "Description=NFSv4 Root File System\n");
	fprintf(f, "SourcePath=/proc/cmdline\n");
	fprintf(f, "Requires=modprobe@sunrpc.service rpc_pipefs.target\n");
	fprintf(f, "Wants=nfs-idmapd.service network-online.target remote-fs-pre.target\n");
	fprintf(f, "After=modprobe@sunrpc.service nfs-idmapd.service network-online.target remote-fs-pre.target rpc_pipefs.target\n");
	fprintf(f, "Before=initrd-root-fs.target remote-fs.target\n");
	fprintf(f, "\n[Mount]\n");
	fprintf(f, "What=%s:%s\n", info->server, info->path);
	fprintf(f, "Where=/sysroot\n");
	fprintf(f, "Type=nfs4\n");
	if (info->options)
		fprintf(f, "Options=%s\n", info->options);

	fclose(f);

	rpath = malloc(strlen(dirname) + 1 + sizeof(dirbase) + sizeof(filebase));
	if (!rpath) {
		ret = ENOMEM;
		goto cleanup;
	}
	sprintf(rpath, "%s%s", dirname, dirbase);
	mkdir(rpath, 0755);
	strcat(rpath, filebase);

	if (symlink(path, rpath) < 0 && errno != EEXIST)
		ret = errno;

	free(rpath);
cleanup:
	free(path);

	return ret;
}

int main(int argc, char *argv[])
{
	int 	ret;
	struct nfsroot_info	info = {
		.server = NULL,
		.path = NULL,
		.options = NULL,
	};

	/* Avoid using any external services */
	xlog_syslog(0);

	if (argc != 4 || argv[1][0] != '/') {
		fprintf(stderr, "nfsroot-generator: mount /sysroot via NFSv4 in the initrd\n");
		fprintf(stderr, "Usage: normal-dir early-dir late-dir\n");
		exit(1);
	}

	if (!systemd_in_initrd())
		exit(0);

	ret = get_nfsroot_info_from_cmdline(&info);
	if (ret) {
		fprintf(stderr, "nfsroot-generator: failed to get nfsroot from kernel command line: %s\n",
			strerror(ret));
		goto cleanup;
	}

	if (info.server && info.path) {
		ret = generate_sysroot_mount_unit(&info, argv[1]);
		if (ret) {
			fprintf(stderr, "nfsroot-generator: failed to generate sysroot.mount unit: %s\n",
				strerror(ret));
		}
	}

cleanup:
	if (info.server)
		free(info.server);
	if (info.path)
		free(info.path);
	if (info.options)
		free(info.options);

	exit(ret);
}
