Imagine that you need to install a service (a daemon) outside on a virtual machine as silently as possible while itโs turned off. This method lets you install all necessary files without involving the user in the process. If this article allows you to save valuable time, then weโve achieved our goal.
Weโll share our experience so you can know how to solve a similar task.
Contents
Why do you need to use this installation method?
External hidden installation of a service on a virtual machine is a convenient method that will allow you to
- install any service without user intervention;
- install the same service on multiple systems.
It may be useful for installing system security or maintenance-related services, such as for user data protection or transferring data to another system.
Typically, a user turns on a system, installs the necessary software, then turns off the system. But using the method described below, you can do the same thing for multiple users without turning their systems on.
The main advantage is that installation takes place when virtual machines arenโt in use, saving usersโ time and not requiring them to take additional action. This method lets you install a service in a centralized manner and lets your users start working at the touch of a button, waiting only two minutes instead of the five to seven with manual installation.
This method can also be an important option when supporting individual user systems if the administrator doesnโt have the opportunity to log in to a userโs machine.
Sample environment
Letโs consider an implementation in a Linux environment. In our example, we have Ubuntu Desktop 16.04 with a standard installation on the ext4 file system as a virtual machine for the QEMU hardware emulation system.
– Ubuntu is a free distribution of the Linux operating system.
– QEMU is a free, open-source virtualization tool thatโs used to emulate various architectures.
– ext4 (also called ext4fs) is the Fourth Extended File System. Itโs a journaling file system running under the Linux operating system. Ext4 was built on the platform of its predecessor, ext3, which is used to be the default file system for many popular GNU/Linux distributions.
How you can do it
We donโt have any ways to directly interact with the virtual machine through the network or by other means. But since itโs a virtual machine, thereโs a disk, which is a regular file that you can modify to add information about starting the service (daemon) and to add the executable file itself. You can mount the disk using standard QEMU tools and work with data on it. You could also parse an image file, but that may take more than a month.
In our example, weโll use the libguestfs library because it has the necessary functionality to perform this task. The utilities in libguestfs use the Linux kernel code and QEMU. They allow you to access almost any type of file system:
- All known Linux file systems (ext2/3/4, XFS, btrfs, etc.)
- Any Windows file system (VFAT and NTFS)
- any macOS and BSD file system
Itโs also possible to access LVM2 volumes, MBR and GPT disk partitions, raw disks, qcow2, VirtualBox VDI, VMWare VMDK, Hyper-V VHD/VHDX, files, local devices, CD and DVD ISOs, SD cards, and remote directories via FTP, HTTP, SSH, iSCSI, NBD, GlusterFS, Ceph, Sheepdog, and so on.
Libguestfs doesnโt require root privileges. In addition, it can access and change images of virtual disks as well as view and edit files inside virtual machines from the outside.
Simple service
For a better understanding libguestfs implementation, letโs start with writing and analyzing the code that starts a simple service (daemon). A daemon is a service of a Unix or Unix-like operating system that runs in the background without direct communication with the user.
Letโs create a simple daemon that will start every five seconds to write a message to the log about the current time.
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/stat.h>
#include <syslog.h>
#include <unistd.h>
void do_heartbeat()
{
time_t rawtime;
struct tm * timeinfo;
time ( &rawtime );
timeinfo = localtime ( &rawtime );
syslog(LOG_NOTICE, "Current local time and date: %s", asctime (timeinfo));
}
int main(void)
{
pid_t pid, sid;
pid = fork();
if(pid > 0)
{
exit(EXIT_SUCCESS);
}
else if(pid < 0)
{
exit(EXIT_FAILURE);
}
umask(0);
openlog("daemon-named", LOG_NOWAIT | LOG_PID, LOG_USER);
syslog(LOG_NOTICE, "Successfully started daemon-name");
sid = setsid();
if(sid < 0)
{
syslog(LOG_ERR, "Could not generate session ID for child process");
exit(EXIT_FAILURE);
}
if((chdir("/")) < 0)
{
syslog(LOG_ERR, "Could not change working directory to /");
exit(EXIT_FAILURE);
}
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
const int SLEEP_INTERVAL = 5;
while(1)
{
do_heartbeat();
sleep(SLEEP_INTERVAL);
}
syslog(LOG_NOTICE, "Stopping daemon-name");
closelog();
exit(EXIT_SUCCESS);
}
We also need the systemd service file to run the script when the system boots. Hereโs an example of a configuration file:
[Unit]
Description= Simple daemon
[Service]
Type=simple
ExecStart=/usr/bin/daemon
[Install]
WantedBy=multi-user.target
Now that we have the daemon and config files, letโs run and test our prototype. Guestfish, which is included in the libguestfs-tools package, provides us with a handy utility with a command-line interpreter. Weโll outline a simple script that will install our service on a powered off virtual machine.
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT=$(guestfish -d generic -i inspect-os)
echo "root disk : $ROOT"
guestfish -d generic <<_EOF_
run
mount $ROOT /
copy-in $DIR/daemon/usr/bin/
copy-in $DIR/simple-daemon.service/etc/systemd/system/
_EOF_
In the service log, you can see that the service has appeared in the system and the executable file is in the right place, /usr/bin/daemon, but it doesnโt work and is disabled.
\u25cf simple-daemon.service - Super simple daemon
Loaded: loaded (/etc/systemd/system/simple-daemon.service; disabled; vendor preset: enabled)
Active: inactive (dead)
Usually, these are the commands to install the daemon in the system:
systemctl daemon-reload
systemctl enable simple-daemon.service
systemctl start simple-daemon.service
But the system is disabled by daemon-reload, which we donโt need, which means that we only need to enable a simple-daemon.service. There are two ways to do this. The first is to enable systemctl, and the second is to see how this operation works and implement it using file operations. As it turns out, the enable operation simply creates a symbolic link, which means that now only one line should be added to the script.
ln-s /etc/systemd/system/simple-daemon.service
/etc/systemd/system/multi-user.target.wants/simple-daemon.service
unmount /
This is what launching a command in guestfish looks like in our case:
command "/bin/systemctl enable simple-daemon.service"
unmount /
After this, we run the script and check the result. Everything works perfectly. Just put the script in order, add some beauty, and hereโs the result:
#!/bin/bash
if [ "$(whoami)" != "root" ]; then
echo "Sorry, you are not root."
exit 1
fi
RED='\033[0;31m'
GREEN='\033[0;32m'
RESET='\033[0m' # No Color
SYSTEMD="/etc/systemd/system"
DAEMON_NAME="simple-daemon.service"
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT=$(guestfish -d generic -i inspect-os)
res=$?
if [[ $res -eq 0 ]]; then
echo "root disk : $ROOT"
else
echo "root disk : ${RED}failed${RESET}"
exit 1
fi
guestfish -d generic <<_EOF_
run
mount $ROOT /
copy-in $DIR/daemon /usr/bin/
copy-in $DIR/$DAEMON_NAME $SYSTEMD/
ln-s $SYSTEMD/$DAEMON_NAME $SYSTEMD/multi-user.target.wants/$DAEMON_NAME
unmount /
_EOF_
res=$?
if [[ $res -eq 0 ]]; then
echo -e "installation: ${GREEN}complete${RESET}"
else
echo -e "installation: ${RED}failed${RESET}"
Final program
In the end, we need to write a program that will perform the actions described in the script. The final program might look like this:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <guestfs.h>
void usage(void)
{
fprintf(stderr,
"Usage: ./install uuid\n"
"\n"
"NOTE: running on live virtual machine or some filesystems could corrupt disk.\n"
"\n");
}
void get_full_path(char *argv[], char (*abs_exe_path)[PATH_MAX])
{
char *p = NULL;
char path_save[PATH_MAX];
if(!(p = strrchr(argv[0], '/')))
{
getcwd(*abs_exe_path, sizeof(*abs_exe_path));
} else
{
*p = '\0';
getcwd(path_save, sizeof(path_save));
chdir(argv[0]);
getcwd(*abs_exe_path, sizeof(*abs_exe_path));
chdir(path_save);
}
}
int main(int argc, char *argv[])
{
guestfs_h *g = NULL;
int res = EXIT_SUCCESS;
const char *uuid = NULL;
struct guestfs_add_domain_argv options = {0};
char **list = NULL, **ptr = NULL, **os_root = NULL, *p = NULL;
char systemd_conf_path[PATH_MAX] = {0}, systemd_conf_link_path[PATH_MAX] = {0};
char abs_exe_path[PATH_MAX] = {0}, daemon_path[PATH_MAX] = {0}, daemon_conf_path[PATH_MAX] = {0};
char daemon_name[] = "daemon";
char daemon_conf[] = "simple-daemon.service";
char bin_path[] = "/usr/bin/";
char systemd_path[] = "/etc/systemd/system/";
if (argc != 2) {
usage();
exit(EXIT_FAILURE);
}
uuid = argv[1];
fflush(stdout);
/* Guestfs handle. */
g = guestfs_create();
if (g == NULL) {
perror("could not create libguestfs handle");
exit(EXIT_FAILURE);
}
/* Add the named domain. */
if (guestfs_add_domain_argv(g, uuid, &options) == -1)
{
exit(EXIT_FAILURE);
}
if (guestfs_launch(g) == -1)
{
perror("could launch");
exit(EXIT_FAILURE);
}
/* inspect os, get root disk */
os_root = guestfs_inspect_os(g);
if (os_root == NULL)
{
perror("could not inspect operation system");
res = EXIT_FAILURE;
goto close;
}
printf("root disk:\t%s\n", *os_root);
/* Mount the domain filesystem. */
if (guestfs_mount(g, *os_root, "/") == -1)
{
perror("could not mount filesystem");
res = EXIT_FAILURE;
goto close;
}
/* set path variable */
get_full_path(argv, &abs_exe_path);
strcpy(daemon_path, abs_exe_path);
strcat(daemon_path, "/");
strcat(daemon_path, daemon_name);
strcpy(daemon_conf_path, abs_exe_path);
strcat(daemon_conf_path, "/");
strcat(daemon_conf_path, daemon_conf);
strcpy(systemd_conf_path, systemd_path);
strcat(systemd_conf_path, daemon_conf);
strcpy(systemd_conf_link_path, systemd_path);
strcat(systemd_conf_link_path, "multi-user.target.wants/");
strcat(systemd_conf_link_path, daemon_conf);
/* copy file */
if (guestfs_copy_in(g, daemon_path, bin_path) == -1) {
perror("could not copy daemon to guest filesystem");
res = EXIT_FAILURE;
goto unmount;
}
if (guestfs_copy_in(g, daemon_conf_path, systemd_path) == -1) {
perror("could not copy daemon config to guest filesystem");
res = EXIT_FAILURE;
goto unmount;
}
/* make link */
if (guestfs_ln_s(g, systemd_conf_path, systemd_conf_link_path) == -1) {
perror("could not make config link");
res = EXIT_FAILURE;
goto unmount;
}
unmount:
printf("installation:\t%s\n", (res == EXIT_SUCCESS ? "complete" : "failed"));
guestfs_umount(g, "/");
close:
guestfs_close(g);
exit(res);
}
Conclusion
After all these uncomplicated manipulations, weโve installed the service (a daemon) in a way thatโs completely imperceptible to the virtual machine user, although to do this we still needed access to the host. This method is quite versatile and is easier than creating many installation packages and maintaining them. This is just a simple introductory example of the large and powerful libguestfs library.
Virtualization-related tasks on various platforms are one of Aprioritโs specialties. While building virtualization software solutions, our team has accumulated a number of useful tips and recipes that we would be glad to share with you.