helper: Print /modules when verbose >= 2
[febootstrap.git] / helper / kernel.c
1 /* febootstrap-supermin-helper reimplementation in C.
2  * Copyright (C) 2009-2010 Red Hat Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17  */
18
19 #include <config.h>
20
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <fnmatch.h>
25 #include <unistd.h>
26 #include <errno.h>
27 #include <sys/utsname.h>
28
29 #include "error.h"
30 #include "xvasprintf.h"
31
32 #include "helper.h"
33
34 /* Directory containing candidate kernels.  We could make this
35  * configurable at some point.
36  */
37 #define KERNELDIR "/boot"
38 #define MODULESDIR "/lib/modules"
39
40 static char* get_kernel_version (char* filename);
41 static const char *create_kernel_from_env (const char *hostcpu, const char *kernel, const char *kernel_env, const char *modpath_env);
42
43 static char *
44 get_modpath (const char *kernel_name)
45 {
46   /* Ignore "vmlinuz-" at the beginning of the kernel name. */
47   const char *version = &kernel_name[8];
48
49   /* /lib/modules/<version> */
50   char *modpath = xasprintf (MODULESDIR "/%s", version);
51   if (!modpath) {
52     perror ("xasprintf");
53     exit (EXIT_FAILURE);
54   }
55
56   if (! isdir (modpath)) {
57     char* path;
58     char* version;
59     path = xasprintf (KERNELDIR "/%s", kernel_name);
60     if (!path) {
61       perror ("xasprintf");
62       exit (EXIT_FAILURE);
63     }
64     version = get_kernel_version (path);
65     free (path);
66     if (version != NULL) {
67       free (modpath);
68       modpath = xasprintf (MODULESDIR "/%s", version);
69       free (version);
70       if (!path) {
71         perror ("xasprintf");
72         exit (EXIT_FAILURE);
73       }
74     }
75   }
76
77   return modpath;
78 }
79
80 /* kernel_name is "vmlinuz-*".  Check if there is a corresponding
81  * module path in /lib/modules.
82  */
83 static int
84 has_modpath (const char *kernel_name)
85 {
86   char *modpath = get_modpath (kernel_name);
87
88   if (verbose)
89     fprintf (stderr, "checking modpath %s is a directory\n", modpath);
90
91   int r = isdir (modpath);
92
93   if (r) {
94     if (verbose)
95       fprintf (stderr, "picked %s because modpath %s exists\n",
96                kernel_name, modpath);
97     free (modpath);
98     return 1;
99   }
100   else {
101     free (modpath);
102     return 0;
103   }
104 }
105
106 /* Create the kernel.  This chooses an appropriate kernel and makes a
107  * symlink to it.
108  *
109  * Look for the most recent kernel named vmlinuz-*.<arch>* which has a
110  * corresponding directory in /lib/modules/. If the architecture is
111  * x86, look for any x86 kernel.
112  *
113  * RHEL 5 didn't append the arch to the kernel name, so look for
114  * kernels without arch second.
115  *
116  * If no suitable kernel can be found, exit with an error.
117  *
118  * This function returns the module path (ie. /lib/modules/<version>).
119  */
120 const char *
121 create_kernel (const char *hostcpu, const char *kernel)
122 {
123   /* Override kernel selection using environment variables? */
124   char *kernel_env = getenv ("FEBOOTSTRAP_KERNEL");
125   if (kernel_env) {
126     char *modpath_env = getenv ("FEBOOTSTRAP_MODULES");
127     return create_kernel_from_env (hostcpu, kernel, kernel_env, modpath_env);
128   }
129
130   /* In original: ls -1dvr /boot/vmlinuz-*.$arch* 2>/dev/null | grep -v xen */
131   const char *patt;
132   if (hostcpu[0] == 'i' && hostcpu[2] == '8' && hostcpu[3] == '6' &&
133       hostcpu[4] == '\0')
134     patt = "vmlinuz-*.i?86*";
135   else
136     patt = xasprintf ("vmlinuz-*.%s*", hostcpu);
137
138   char **all_files = read_dir (KERNELDIR);
139   char **candidates;
140   candidates = filter_fnmatch (all_files, patt, FNM_NOESCAPE);
141   candidates = filter_notmatching_substring (candidates, "xen");
142   candidates = filter (candidates, has_modpath);
143
144   if (candidates[0] == NULL) {
145     /* In original: ls -1dvr /boot/vmlinuz-* 2>/dev/null | grep -v xen */
146     patt = "vmlinuz-*";
147     candidates = filter_fnmatch (all_files, patt, FNM_NOESCAPE);
148     candidates = filter_notmatching_substring (candidates, "xen");
149     candidates = filter (candidates, has_modpath);
150
151     if (candidates[0] == NULL)
152       goto no_kernels;
153   }
154
155   sort (candidates, reverse_filevercmp);
156
157   if (kernel) {
158     /* Choose the first candidate. */
159     char *tmp = xasprintf (KERNELDIR "/%s", candidates[0]);
160
161     if (verbose >= 2)
162       fprintf (stderr, "creating symlink %s -> %s\n", kernel, tmp);
163
164     if (symlink (tmp, kernel) == -1)
165       error (EXIT_FAILURE, errno, "symlink kernel");
166
167     free (tmp);
168   }
169
170   return get_modpath (candidates[0]);
171
172   /* Print more diagnostics here than the old script did. */
173  no_kernels:
174   fprintf (stderr,
175            "febootstrap-supermin-helper: failed to find a suitable kernel.\n"
176            "I looked for kernels in " KERNELDIR " and modules in " MODULESDIR
177            ".\n"
178            "If this is a Xen guest, and you only have Xen domU kernels\n"
179            "installed, try installing a fullvirt kernel (only for\n"
180            "febootstrap use, you shouldn't boot the Xen guest with it).\n");
181   exit (EXIT_FAILURE);
182 }
183
184 /* Select the kernel from environment variables set by the user.
185  * modpath_env may be NULL, in which case we attempt to work it out
186  * from kernel_env.
187  */
188 static const char *
189 create_kernel_from_env (const char *hostcpu, const char *kernel,
190                         const char *kernel_env, const char *modpath_env)
191 {
192   if (verbose) {
193     fprintf (stderr,
194              "febootstrap-supermin-helper: using environment variable(s) FEBOOTSTRAP_* to\n"
195              "select kernel %s", kernel_env);
196     if (modpath_env)
197       fprintf (stderr, " and module path %s", modpath_env);
198     fprintf (stderr, "\n");
199   }
200
201   if (!isfile (kernel_env)) {
202     fprintf (stderr,
203              "febootstrap-supermin-helper: %s: not a regular file\n"
204              "(what is $FEBOOTSTRAP_KERNEL set to?)\n", kernel_env);
205     exit (EXIT_FAILURE);
206   }
207
208   if (!modpath_env) {
209     /* Try to guess modpath from kernel path. */
210     const char *p = strrchr (kernel_env, '/');
211     if (p) p++; else p = kernel_env;
212
213     /* NB: We need the extra test to ensure calling get_modpath is safe. */
214     if (strncmp (p, "vmlinuz-", 8) != 0) {
215       fprintf (stderr,
216                "febootstrap-supermin-helper: cannot guess module path.\n"
217                "Set $FEBOOTSTRAP_MODULES to the modules directory corresponding to\n"
218                "kernel %s, or unset $FEBOOTSTRAP_KERNEL to autoselect a kernel.\n",
219                kernel_env);
220       exit (EXIT_FAILURE);
221     }
222
223     modpath_env = get_modpath (p);
224   }
225
226   if (!isdir (modpath_env)) {
227     fprintf (stderr,
228              "febootstrap-supermin-helper: %s: not a directory\n"
229              "(what is $FEBOOTSTRAP_MODULES set to?)\n", modpath_env);
230     exit (EXIT_FAILURE);
231   }
232
233   /* Create the symlink. */
234   if (kernel) {
235     if (verbose >= 2)
236       fprintf (stderr, "creating symlink %s -> %s\n", kernel_env, kernel);
237
238     if (symlink (kernel_env, kernel) == -1)
239       error (EXIT_FAILURE, errno, "symlink kernel");
240   }
241
242   return modpath_env;
243 }
244
245 /* Read an unsigned little endian short at a specified offset in a file.
246  * Returns a non-negative int on success or -1 on failure.
247  */
248 static int
249 read_leshort (FILE* fp, int offset)
250 {
251   char buf[2];
252   if (fseek (fp, offset, SEEK_SET) != 0 ||
253       fread (buf, sizeof(char), 2, fp) != 2)
254   {
255     return -1;
256   }
257   return ((buf[1] & 0xFF) << 8) | (buf[0] & 0xFF);
258 }
259
260 /* Extract the kernel version from a Linux kernel file.
261  * Returns a malloc'd string containing the version or NULL if the
262  * file can't be read, is not a Linux kernel, or the version can't
263  * be found.
264  *
265  * See ftp://ftp.astron.com/pub/file/file-<ver>.tar.gz
266  * (file-<ver>/magic/Magdir/linux) for the rules used to find the
267  * version number:
268  *   514             string  HdrS     Linux kernel
269  *   >518            leshort >0x1ff
270  *   >>(526.s+0x200) string  >\0      version %s,
271  *
272  * Bugs: probably limited to x86 kernels.
273  */
274 static char*
275 get_kernel_version (char* filename)
276 {
277   FILE* fp;
278   int size = 132;
279   char buf[size];
280   int offset;
281
282   fp = fopen (filename, "rb");
283
284   if (fseek (fp, 514, SEEK_SET) != 0 ||
285       fgets (buf, size, fp) == NULL ||
286       strncmp (buf, "HdrS", 4) != 0 ||
287       read_leshort (fp, 518) < 0x1FF)
288   {
289     /* not a Linux kernel */
290     fclose (fp);
291     return NULL;
292   }
293
294   offset = read_leshort (fp, 526);
295   if (offset == -1)
296   {
297     /* can't read version offset */
298     fclose (fp);
299     return NULL;
300   }
301
302   if (fseek (fp, offset + 0x200, SEEK_SET) != 0 ||
303       fgets (buf, size, fp) == NULL)
304   {
305     /* can't read version string */
306     fclose (fp);
307     return NULL;
308   }
309
310   fclose (fp);
311
312   buf[strcspn (buf, " \t\n")] = '\0';
313   return strdup (buf);
314 }