1#!/usr/bin/env python3

2

3"""

4 Script to test that the system PXE-booted, if run in EFI mode

5

6 Copyright (c) 2016 Canonical Ltd.

7

8 Authors

9 Rod Smith <rod.smith@canonical.com>

10

11 This program is free software: you can redistribute it and/or modify

12 it under the terms of the GNU General Public License version 3,

13 as published by the Free Software Foundation.

14

15 This program is distributed in the hope that it will be useful,

16 but WITHOUT ANY WARRANTY; without even the implied warranty of

17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

18 GNU General Public License for more details.

19

20 You should have received a copy of the GNU General Public License

21 along with this program. If not, see <http://www.gnu.org/licenses/>.

22

23 The purpose of this script is to identify whether an EFI-based

24 system booted from the network (test passes) or from a local disk

25(test fails).

26

27 Usage:

28 efi-pxeboot.py

29"""

30

31 import os

32 import shlex

33 import shutil

34 import sys

35

36 from subprocess import Popen, PIPE

37

38

39 def discover_data():

40 """Extract boot entry and boot order information.

41

42 :returns:

43 boot_entries, boot_order, boot_current

44 """

45 command = "efibootmgr -v"

46 bootinfo_bytes = Popen(shlex.split(command), stdout=PIPE).communicate()[0]

47 bootinfo = bootinfo_bytes.decode(

48 encoding="utf-8", errors="ignore"

49 ).splitlines()

50 boot_entries = {}

51 boot_order = []

52 boot_current = ""

53 if len(bootinfo) > 1:

54 for s in bootinfo:

55 if "BootOrder" in s:

56 try:

57 boot_order = s.split(":")[1].replace(" ", "").split(",")

58 except IndexError:

59 pass

60 elif "BootCurrent" in s:

61 try:

62 boot_current = s.split(":")[1].strip()

63 except IndexError:

64 pass

65 else:

66 # On Boot#### lines, #### is characters 4-8....

67 hex_value = s[4:8]

68 # ....and the description starts at character 10

69 name = s[10:]

70 try:

71 # In normal efibootmgr output, only Boot#### entries

72 # have characters 4-8 that can be interpreted as

73 # hex values, so this will harmlessly error out on all

74 # but Boot#### entries....

75 int(hex_value, 16)

76 boot_entries[hex_value] = name

77 except ValueError:

78 pass

79 return boot_entries, boot_order, boot_current

80

81

82 def is_pxe_booted(boot_entries, boot_order, boot_current):

83 retval = 0

84 desc = boot_entries[boot_current]

85 print("The current boot item is {}".format(boot_current))

86 print("The first BootOrder item is {}".format(boot_order[0]))

87 print("The description of Boot{} is '{}'".format(boot_current, desc))

88 if boot_current != boot_order[0]:

89 # If the BootCurrent entry isn't the same as the first of the

90 # BootOrder entries, then something is causing the first boot entry

91 # to fail or be bypassed. This could be a Secure Boot failure, manual

92 # intervention, a bad boot entry, etc. This is not necessarily a

93 # problem, but warn of it anyhow....

94 desc2 = boot_entries[boot_order[0]]

95 print("The description of Boot{} is '{}'".format(boot_order[0], desc2))

96 print(

97 "WARNING: The system is booted using Boot{}, but the first".format(

98 boot_current

99 )

100 )

101 print("boot item is Boot{}!".format(boot_order[0]))

102 if (

103 "Network" in desc

104 or "PXE" in desc

105 or "NIC" in desc

106 or "Ethernet" in desc

107 or "IP4" in desc

108 or "IP6" in desc

109 ):

110 # These strings are present in network-boot descriptions.

111 print("The system seems to have PXE-booted; all OK.")

112 elif (

113 "ubuntu" in desc

114 or "grub" in desc

115 or "shim" in desc

116 or "rEFInd"

117 or "refind_" in desc

118 ):

119 # This string indicates a boot directly from the normal Ubuntu GRUB

120 # or rEFInd installation on the hard disk.

121 print("FAIL: The system has booted directly from the hard disk!")

122 retval = 1

123 elif "SATA" in desc or "Sata" in desc or "Hard Drive" in desc:

124 # These strings indicate booting with a "generic" disk entry (one

125 # that uses the fallback filename, EFI/BOOT/bootx64.efi or similar).

126 print(

127 "The system seems to have booted from a disk with the fallback "

128 "boot loader!"

129 )

130 retval = 1

131 else:

132 # Probably a rare description. Call it an error so we can flag it and

133 # improve this script.

134 print("Unable to identify boot path.")

135 retval = 1

136 return retval

137

138

139 def main():

140 """Check to see if the system PXE-booted."""

141

142 if shutil.which("efibootmgr") is None:

143 print("The efibootmgr utility is not installed; exiting!")

144 return 4

145 if not os.geteuid() == 0:

146 print("This program must be run as root (or via sudo); exiting!")

147 return 4

148

149 retval = 0

150 boot_entries, boot_order, boot_current = discover_data()

151 if boot_entries == {}:

152 print(

153 "No EFI boot entries are available. This may indicate a "

154 "firmware problem."

155 )

156 retval = 2

157 if boot_order == []:

158 print(

159 "The EFI BootOrder variable is not available. This may "

160 "indicate a firmware"

161 )

162 print("problem.")

163 retval = 3

164 if boot_current == "":

165 print(

166 "FAIL: The EFI BootCurrent variable is missing. This may "

167 "indicate a firmware"

168 )

169 print("problem.")

170 retval = 4

171 if retval == 0:

172 retval = is_pxe_booted(boot_entries, boot_order, boot_current)

173 return retval

174

175

176 if __name__ == "__main__":

177 sys.exit(main())