kexecme

Easily kexec (on Fedora)

git clone https://code.pdelong.com/kexecme.git

  1package main
  2
  3import (
  4	"bufio"
  5	"fmt"
  6	"io"
  7	"io/fs"
  8	"io/ioutil"
  9	"os"
 10	"path/filepath"
 11	"sort"
 12	"strconv"
 13	"strings"
 14
 15	dbus "github.com/godbus/dbus/v5"
 16	"golang.org/x/sys/unix"
 17)
 18
 19const (
 20	BOOT_ENTRIES_DIR       = "/boot/loader/entries"
 21	SD_LOGIND_KEXEC_REBOOT = uint64(2)
 22)
 23
 24// kernelInfo contains information for a single Grub kernel entry.
 25type kernelInfo struct {
 26	title   string
 27	version string
 28	linux   string
 29	initrd  string
 30	options string
 31}
 32
 33func run() error {
 34	files, err := getGrubEntryFiles(BOOT_ENTRIES_DIR)
 35	if err != nil {
 36		return err
 37	}
 38
 39	kernels, err := parseGrubEntryFiles(files)
 40	if err != nil {
 41		return err
 42	}
 43
 44	// FIXME: Sorting is lexicographic rather than numeric.
 45	sort.Slice(kernels, func(i, j int) bool {
 46		return kernels[i].version > kernels[j].version
 47	})
 48
 49	n, err := userSelectKernel(os.Stdin, os.Stdout, kernels)
 50	if err != nil {
 51		return err
 52	}
 53
 54	// If EOF was reached.  Treat this differently from the user hitting just
 55	// the enter key.  Prevents accidentally rebooting if stdin is closed (e.g.
 56	// Ctrl-D or some sort of breakage).
 57	if n == -1 {
 58		return fmt.Errorf("operation aborted")
 59	}
 60
 61	err = kexecFileLoad(kernels[n])
 62	if err != nil {
 63		return err
 64	}
 65
 66	err = reboot()
 67	if err != nil {
 68		return err
 69	}
 70
 71	return nil
 72}
 73
 74func getGrubEntryFiles(dir string) ([]string, error) {
 75	files := []string{}
 76	err := filepath.WalkDir(
 77		dir,
 78		func(path string, d fs.DirEntry, err error) error {
 79			if err != nil {
 80				return err
 81			}
 82
 83			if strings.HasSuffix(path, ".conf") && d.Type().IsRegular() {
 84				files = append(files, path)
 85			}
 86
 87			return nil
 88		})
 89	if err != nil {
 90		return nil, err
 91	}
 92
 93	return files, nil
 94}
 95
 96func parseGrubEntry(contents string) (*kernelInfo, error) {
 97	lines := strings.Split(string(contents), "\n")
 98	infoHash := make(map[string]string, 0)
 99	for _, line := range lines {
100		line = strings.TrimSpace(line)
101		if line == "" {
102			continue
103		}
104
105		k, v, found := strings.Cut(line, " ")
106		if !found {
107			return nil, fmt.Errorf("file in wrong format")
108		}
109
110		infoHash[k] = v
111	}
112
113	info := &kernelInfo{}
114
115	if val, ok := infoHash["title"]; ok {
116		info.title = val
117	} else {
118		return nil, fmt.Errorf("File did not contain title")
119	}
120
121	if val, ok := infoHash["version"]; ok {
122		info.version = val
123	} else {
124		return nil, fmt.Errorf("File did not contain version")
125	}
126
127	if val, ok := infoHash["linux"]; ok {
128		info.linux = val
129	} else {
130		return nil, fmt.Errorf("File did not contain linux")
131	}
132
133	if val, ok := infoHash["initrd"]; ok {
134		info.initrd = val
135	} else {
136		return nil, fmt.Errorf("File did not contain initrd")
137	}
138
139	if val, ok := infoHash["options"]; ok {
140		info.options = val
141	} else {
142		return nil, fmt.Errorf("File did not contain options")
143	}
144
145	return info, nil
146}
147
148func parseGrubEntryFiles(files []string) ([]kernelInfo, error) {
149	infos := make([]kernelInfo, len(files))
150	for i, file := range files {
151		contents, err := ioutil.ReadFile(file)
152		if err != nil {
153			return nil, err
154		}
155
156		entry, err := parseGrubEntry(string(contents))
157		if err != nil {
158			return nil, err
159		}
160
161		infos[i] = *entry
162	}
163
164	return infos, nil
165}
166
167func userSelectKernel(
168	in io.Reader,
169	out io.Writer,
170	kernels []kernelInfo,
171) (int, error) {
172	for i, info := range kernels {
173		fmt.Fprintf(out, "%d\t%s\n", i, info.version)
174	}
175	prompt := fmt.Sprintf("Select a kernel (0-%d)[0] ", len(kernels)-1)
176
177	s := bufio.NewScanner(in)
178	fmt.Fprint(out, prompt)
179	for s.Scan() {
180		line := s.Text()
181		if len(line) == 0 {
182			return 0, nil
183		}
184
185		num, err := strconv.Atoi(line)
186		if err != nil || num < 0 || num >= len(kernels) {
187			fmt.Fprintln(out, "Invalid input")
188			fmt.Fprint(out, prompt)
189			continue
190		}
191
192		return num, nil
193	}
194
195	if s.Err() != nil {
196		return -1, s.Err()
197	}
198
199	return -1, nil
200}
201
202func kexecFileLoad(kernel kernelInfo) error {
203	// Prepare to call kexec.  kexec_file_load has the following signature
204	// (stolen from the manpage):
205	//
206	// long kexec_file_load(int kernel_fd, int initrd_fd,
207	//                      unsigned long cmdline_len, const char *cmdline,
208	//                      unsigned long flags);
209	//
210	// This means we need to open the kernel and initrd files first, but the
211	// rest of the parameters we can pass in directly.
212
213	// Multiple initrd and kernel files are not supported by kexec, so take
214	// whatever comes first.  A side effect of this is that this tool doesn't
215	// support Grub variables, but that should not be a problem in most cases.
216	kernelFirstPart, _, _ := strings.Cut(kernel.linux, " ")
217	kernelFile, err := os.Open("/boot/" + kernelFirstPart)
218	if err != nil {
219		return err
220	}
221
222	// Same as above.
223	initrdFirstPart, _, _ := strings.Cut(kernel.initrd, " ")
224	initrdFile, err := os.Open("/boot/" + initrdFirstPart)
225	if err != nil {
226		return err
227	}
228
229	return unix.KexecFileLoad(
230		int(kernelFile.Fd()), int(initrdFile.Fd()), kernel.options, 0)
231}
232
233// reboot tells logind to reboot the system and kexec.
234//
235// One can also use the reboot syscall with the LINUX_REBOOT_CMD_KEXEC flag,
236// but this performs the kexec immediately.  Using logind allows us to go
237// through most of the shutdown process before calling kexec (notably, flushing
238// buffers, syncing, and stopping services).
239//
240// In the success case, it's unlikely that this function will return.
241func reboot() error {
242	bus, err := dbus.SystemBus()
243	if err != nil {
244		return err
245	}
246	defer bus.Close()
247
248	obj := bus.Object("org.freedesktop.login1", "/org/freedesktop/login1")
249
250	call := obj.Call(
251		"org.freedesktop.login1.Manager.RebootWithFlags",
252		0,
253		SD_LOGIND_KEXEC_REBOOT,
254	)
255	if call.Err != nil {
256		return call.Err
257	}
258
259	return nil
260}
261
262func main() {
263	err := run()
264	if err != nil {
265		fmt.Printf("Command failed with: %s\n", err)
266		os.Exit(1)
267	}
268}