Super User

Silently Installing a Service on a Virtual Machine Using Libguestfs

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.

Written by: Andrey Kuzmenko, Software Designer, Data Recovery Team

Contents:

Why do you need to use this installation method?

Sample environment

How you can do it

Simple service

Final program

Conclusion

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.

Related services Cloud Computing & Virtualization Development

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

" "

" "NOTE: running on live virtual machine or some filesystems could corrupt disk.

" "

"); } 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

", *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

", (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.