File: firstboot.ml

package info (click to toggle)
libguestfs 1%3A1.44.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 118,932 kB
  • sloc: ansic: 458,017; ml: 51,424; sh: 13,191; java: 9,578; makefile: 7,931; cs: 6,328; haskell: 5,674; python: 3,871; perl: 3,528; erlang: 2,446; xml: 1,347; ruby: 350; pascal: 257; javascript: 157; lex: 135; yacc: 128; cpp: 10
file content (367 lines) | stat: -rw-r--r-- 13,029 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
(* virt-customize
 * Copyright (C) 2012-2019 Red Hat Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *)

open Printf

open Std_utils
open Tools_utils
open Common_gettext.Gettext

open Regedit

let sanitize_name =
  let rex = PCRE.compile ~caseless:true "[^a-z0-9_]" in
  fun n ->
    let n = PCRE.replace ~global:true rex "-" n in
    let len = String.length n and max = 60 in
    if len >= max then String.sub n 0 max else n

(* For Linux guests. *)
module Linux = struct
  let firstboot_dir = "/usr/lib/virt-sysprep"

  let firstboot_sh = sprintf "\
#!/bin/sh -

### BEGIN INIT INFO
# Provides:          virt-sysprep
# Required-Start:    $null
# Should-Start:      $all
# Required-Stop:     $null
# Should-Stop:       $all
# Default-Start:     2 3 5
# Default-Stop:      0 1 6
# Short-Description: Start scripts to run once at next boot
# Description:       Start scripts to run once at next boot
#	These scripts run the first time the guest boots,
#	and then are deleted. Output or errors from the scripts
#	are written to ~root/virt-sysprep-firstboot.log.
### END INIT INFO

d=%s/scripts
d_done=%s/scripts-done
logfile=~root/virt-sysprep-firstboot.log

echo \"$0\" \"$@\" 2>&1 | tee -a $logfile
echo \"Scripts dir: $d\" 2>&1 | tee -a $logfile

if test \"$1\" = \"start\"
then
  mkdir -p $d_done
  for f in $d/* ; do
    if test -x \"$f\"
    then
      # move the script to the 'scripts-done' directory, so it is not
      # executed again at the next boot
      mv $f $d_done
      echo '=== Running' $f '===' 2>&1 | tee -a $logfile
      $d_done/$(basename $f) 2>&1 | tee -a $logfile
    fi
  done
  rm -f $d_done/*
fi
" firstboot_dir firstboot_dir

  let systemd_target = "multi-user.target"

  let firstboot_service = sprintf "\
[Unit]
Description=libguestfs firstboot service
After=network.target
Before=prefdm.service

[Service]
Type=oneshot
ExecStart=%s/firstboot.sh start
RemainAfterExit=yes
StandardOutput=journal+console
StandardError=inherit

[Install]
WantedBy=%s
" firstboot_dir systemd_target

  let rec install_service (g : Guestfs.guestfs) root distro major =
    g#mkdir_p firstboot_dir;
    g#mkdir_p (sprintf "%s/scripts" firstboot_dir);
    g#write (sprintf "%s/firstboot.sh" firstboot_dir) firstboot_sh;
    g#chmod 0o755 (sprintf "%s/firstboot.sh" firstboot_dir);

    (* Note we install both systemd and sysvinit services.  This is
     * because init systems can be switched at runtime, and it's easy to
     * tell if systemd is installed (eg. Ubuntu uses upstart but installs
     * systemd configuration directories).  There is no danger of a
     * firstboot script running twice because they disable themselves
     * after running.
     *)
    if g#is_dir "/etc/systemd/system" then
      install_systemd_service g;
    if g#is_dir "/etc/rc.d" || g#is_dir "/etc/init.d" then
      install_sysvinit_service g root distro major

  (* Install the systemd firstboot service, if not installed already. *)
  and install_systemd_service g =
    (* RHBZ#1250955: systemd will only recognize unit files located
     * in /usr/lib/systemd/system/
     *)
    let unitdir = "/usr/lib/systemd/system" in
    g#mkdir_p unitdir;
    let unitfile = sprintf "%s/guestfs-firstboot.service" unitdir in
    g#write unitfile firstboot_service;
    g#mkdir_p (sprintf "/etc/systemd/system/%s.wants"
                       systemd_target);
    g#ln_sf unitfile (sprintf "/etc/systemd/system/%s.wants"
                              systemd_target);

    (* Try to remove the old firstboot.service files. *)
    let oldunitfile = sprintf "%s/firstboot.service" unitdir in
    if g#is_file oldunitfile then (
      g#rm_f "/etc/systemd/system/default.target.wants/firstboot.service";
      (* Remove the old firstboot.service only if it is one of our
       * versions. *)
      match g#checksum "md5" oldunitfile with
      | "6923781f7a1851b40b32b4960eb9a0fc"  (* < 1.23.24 *)
      | "56fafd8c990fc9d24e5b8497f3582e8d"  (* < 1.23.32 *)
      | "a83767e01cf398e2fd7c8f59d65d320a"  (* < 1.25.2 *)
      | "39aeb10df29104797e3a9aca4db37a6e" ->
        g#rm oldunitfile
      | csum ->
        warning (f_"firstboot: unknown version for old firstboot.service file %s (md5=%s), it will not be removed")
          oldunitfile csum
    );
    (* And the old default.target.wants/guestfs-firstboot.service from
     * libguestfs <= 1.37.17.
     *)
    g#rm_f "/etc/systemd/system/default.target.wants/guestfs-firstboot.service"

  and install_sysvinit_service g root distro major =
    match distro with
    | "fedora"|"rhel"|"centos"|"scientificlinux"|"oraclelinux"|"redhat-based" ->
      install_sysvinit_redhat g
    | "opensuse"|"sles"|"suse-based" ->
      install_sysvinit_suse g
    | ("debian"|"kalilinux") ->
      install_sysvinit_debian g;
      if major <= 7 then try_update_rc_d g root
    | "ubuntu" ->
      install_sysvinit_debian g
    | distro ->
      error (f_"guest type %s is not supported") distro

  and install_sysvinit_redhat g =
    g#mkdir_p "/etc/rc.d/rc2.d";
    g#mkdir_p "/etc/rc.d/rc3.d";
    g#mkdir_p "/etc/rc.d/rc5.d";
    g#ln_sf (sprintf "%s/firstboot.sh" firstboot_dir)
      "/etc/rc.d/rc2.d/S99guestfs-firstboot";
    g#ln_sf (sprintf "%s/firstboot.sh" firstboot_dir)
      "/etc/rc.d/rc3.d/S99guestfs-firstboot";
    g#ln_sf (sprintf "%s/firstboot.sh" firstboot_dir)
      "/etc/rc.d/rc5.d/S99guestfs-firstboot";

    (* Try to remove the files of the old service. *)
    g#rm_f "/etc/rc.d/rc2.d/S99virt-sysprep-firstboot";
    g#rm_f "/etc/rc.d/rc3.d/S99virt-sysprep-firstboot";
    g#rm_f "/etc/rc.d/rc5.d/S99virt-sysprep-firstboot"

  (* Make firstboot.sh look like a runlevel script to avoid insserv warnings. *)
  and install_sysvinit_suse g =
    g#mkdir_p "/etc/init.d/rc2.d";
    g#mkdir_p "/etc/init.d/rc3.d";
    g#mkdir_p "/etc/init.d/rc5.d";
    g#ln_sf (sprintf "%s/firstboot.sh" firstboot_dir)
      "/etc/init.d/guestfs-firstboot";
    g#ln_sf "../guestfs-firstboot"
      "/etc/init.d/rc2.d/S99guestfs-firstboot";
    g#ln_sf "../guestfs-firstboot"
      "/etc/init.d/rc3.d/S99guestfs-firstboot";
    g#ln_sf "../guestfs-firstboot"
      "/etc/init.d/rc5.d/S99guestfs-firstboot";

    (* Try to remove the files of the old service. *)
    g#rm_f "/etc/init.d/virt-sysprep-firstboot";
    g#rm_f "/etc/init.d/rc2.d/S99virt-sysprep-firstboot";
    g#rm_f "/etc/init.d/rc3.d/S99virt-sysprep-firstboot";
    g#rm_f "/etc/init.d/rc5.d/S99virt-sysprep-firstboot"

  and install_sysvinit_debian g =
    g#mkdir_p "/etc/init.d";
    g#mkdir_p "/etc/rc2.d";
    g#mkdir_p "/etc/rc3.d";
    g#mkdir_p "/etc/rc5.d";
    g#ln_sf (sprintf "%s/firstboot.sh" firstboot_dir)
      "/etc/init.d/guestfs-firstboot";
    g#ln_sf "../init.d/guestfs-firstboot"
      "/etc/rc2.d/S99guestfs-firstboot";
    g#ln_sf "../init.d/guestfs-firstboot"
      "/etc/rc3.d/S99guestfs-firstboot";
    g#ln_sf "../init.d/guestfs-firstboot"
      "/etc/rc5.d/S99guestfs-firstboot";

    (* Try to remove the files of the old service. *)
    g#rm_f "/etc/init.d/virt-sysprep-firstboot";
    g#rm_f "/etc/rc2.d/S99virt-sysprep-firstboot";
    g#rm_f "/etc/rc3.d/S99virt-sysprep-firstboot";
    g#rm_f "/etc/rc5.d/S99virt-sysprep-firstboot"

  (* On Debian 6 & 7 you have to run: update-rc.d guestfs-firstboot defaults
   * RHBZ#1019388.
   *)
  and try_update_rc_d g root =
    let guest_arch = g#inspect_get_arch root in
    let guest_arch_compatible = guest_arch_compatible guest_arch in
    let cmd = "update-rc.d guestfs-firstboot defaults" in
    if guest_arch_compatible then
      try ignore (g#sh cmd)
      with Guestfs.Error msg ->
        warning (f_"could not finish firstboot installation by running ā€˜%s’ because the command failed: %s")
                cmd msg
    else (
      warning (f_"cannot finish firstboot installation by running ā€˜%s’ because host cpu (%s) and guest arch (%s) are not compatible.  The firstboot service may not run at boot.")
              cmd Guestfs_config.host_cpu guest_arch
    )
end

module Windows = struct
  let rec install_service (g : Guestfs.guestfs) root =
    (* Either rhsrvany.exe or pvvxsvc.exe must exist.
     *
     * (Check also that it's not a dangling symlink but a real file).
     *)
    let services = ["rhsrvany.exe"; "pvvxsvc.exe"] in
    let srvany =
      try
        List.find (
          fun service -> Sys.file_exists (virt_tools_data_dir () // service)
        ) services
      with Not_found ->
       error (f_"One of rhsrvany.exe or pvvxsvc.exe is missing in %s.  One of them is required in order to install Windows firstboot scripts.  You can get one by building rhsrvany (https://github.com/rwmjones/rhsrvany)")
             (virt_tools_data_dir ()) in

    (* Create a directory for firstboot files in the guest. *)
    let firstboot_dir, firstboot_dir_win =
      let rec loop firstboot_dir firstboot_dir_win = function
        | [] -> firstboot_dir, firstboot_dir_win
        | dir :: path ->
          let firstboot_dir =
            if firstboot_dir = "" then "/" ^ dir else firstboot_dir // dir in
          let firstboot_dir_win = firstboot_dir_win ^ "\\" ^ dir in
          let firstboot_dir = g#case_sensitive_path firstboot_dir in
          g#mkdir_p firstboot_dir;
          loop firstboot_dir firstboot_dir_win path
      in
      loop "" "C:" ["Program Files"; "Guestfs"; "Firstboot"] in

    g#mkdir_p (firstboot_dir // "scripts");

    (* Copy pvvxsvc or rhsrvany to the guest. *)
    g#upload (virt_tools_data_dir () // srvany) (firstboot_dir // srvany);

    (* Write a firstboot.bat control script which just runs the other
     * scripts in the directory.  Note we need to use CRLF line endings
     * in this script.
     *)
    let firstboot_script = sprintf "\
@echo off

setlocal EnableDelayedExpansion
set firstboot=%s
set log=%%firstboot%%\\log.txt

set scripts=%%firstboot%%\\scripts
set scripts_done=%%firstboot%%\\scripts-done

call :main >> \"%%log%%\" 2>&1
exit /b

:main
echo starting firstboot service

if not exist \"%%scripts_done%%\" (
  mkdir \"%%scripts_done%%\"
)

for %%%%f in (\"%%scripts%%\"\\*.bat) do (
  echo running \"%%%%f\"
  move \"%%%%f\" \"%%scripts_done%%\"
  pushd \"%%scripts_done%%\"
  call \"%%%%~nf\"
  set elvl=!errorlevel!
  echo .... exit code !elvl!
  popd
)

echo uninstalling firstboot service
\"%%firstboot%%\\%s\" -s firstboot uninstall
" firstboot_dir_win srvany in

    g#write (firstboot_dir // "firstboot.bat")
            (String.unix2dos firstboot_script);

    (* Open the SYSTEM hive. *)
    Registry.with_hive_write g (g#inspect_get_windows_system_hive root)
      (fun reg ->
        let current_cs = g#inspect_get_windows_current_control_set root in

        (* Add a new rhsrvany service to the system registry to execute
         * firstboot.  NB: All these edits are in the HKLM\SYSTEM hive.
         * No other hive may be modified here.
         *)
        let regedits = [
          [ current_cs; "services"; "firstboot" ],
          [ "Type", REG_DWORD 0x10_l;
            "Start", REG_DWORD 0x2_l;
            "ErrorControl", REG_DWORD 0x1_l;
            "ImagePath",
            REG_SZ (sprintf "%s\\%s -s firstboot" firstboot_dir_win srvany);
            "DisplayName", REG_SZ "Virt tools firstboot service";
            "ObjectName", REG_SZ "LocalSystem" ];

          [ current_cs; "services"; "firstboot"; "Parameters" ],
          [ "CommandLine",
            REG_SZ ("cmd /c \"" ^ firstboot_dir_win ^ "\\firstboot.bat\"");
            "PWD", REG_SZ firstboot_dir_win ];
        ] in
        reg_import reg regedits
      );

    firstboot_dir
end

let script_count = ref 0

let add_firstboot_script (g : Guestfs.guestfs) root name content =
  let typ = g#inspect_get_type root in
  let distro = g#inspect_get_distro root in
  let major = g#inspect_get_major_version root in
  incr script_count;
  let filename = sprintf "%04d-%s" !script_count (sanitize_name name) in
  match typ, distro with
  | "linux", _ ->
    Linux.install_service g root distro major;
    let filename = Linux.firstboot_dir // "scripts" // filename in
    g#write filename content;
    g#chmod 0o755 filename

  | "windows", _ ->
    let firstboot_dir = Windows.install_service g root in
    let filename = firstboot_dir // "scripts" // filename ^ ".bat" in
    g#write filename (String.unix2dos content)

  | _ ->
    error (f_"guest type %s/%s is not supported") typ distro