uefi.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package uefi
  15. import (
  16. "fmt"
  17. "strconv"
  18. "strings"
  19. "yunion.io/x/log"
  20. "yunion.io/x/pkg/errors"
  21. "yunion.io/x/onecloud/pkg/cloudcommon/types"
  22. "yunion.io/x/onecloud/pkg/util/regutils2"
  23. "yunion.io/x/onecloud/pkg/util/ssh"
  24. )
  25. const (
  26. // efibootmgr useage: https://github.com/rhboot/efibootmgr
  27. CMD_EFIBOOTMGR = "/usr/sbin/efibootmgr"
  28. SUDO_EFIBOOTMGR = "sudo efibootmgr"
  29. MAC_KEYWORD = "MAC"
  30. )
  31. type BootMgr struct {
  32. // bootCurrent - the boot entry used to start the currently running system.
  33. bootCurrent string
  34. // bootOrder - the boot order as would appear in the boot manager.
  35. // The boot manager tries to boot the first active entry on this list.
  36. // If unsuccessful, it tries the next entry, and so on.
  37. bootOrder []string
  38. // bootNext - the boot entry which is scheduled to be run on next boot.
  39. // This superceeds BootOrder for one boot only, and is deleted by the
  40. // boot manager after first use.
  41. // This allows you to change the next boot behavior without changing BootOrder.
  42. bootNext string
  43. // timeout - the time in seconds between when the boot manager appears on the screen
  44. // until when it automatically chooses the startup value from BootNext or BootOrder.
  45. timeout int
  46. // entries - the boot entry parsed in map
  47. entries map[string]*BootEntry
  48. }
  49. type BootEntry struct {
  50. BootNum string
  51. Description string
  52. IsActive bool
  53. }
  54. func getEFIBootMgrCmd(sudo bool) string {
  55. if sudo {
  56. return SUDO_EFIBOOTMGR
  57. }
  58. return CMD_EFIBOOTMGR
  59. }
  60. func ParseEFIBootMGR(input string) (*BootMgr, error) {
  61. lines := strings.Split(input, "\n")
  62. mgr := &BootMgr{
  63. bootOrder: []string{},
  64. timeout: -1,
  65. entries: make(map[string]*BootEntry),
  66. }
  67. pf := func(ff func(string) bool) {
  68. for _, l := range lines {
  69. if ok := ff(l); ok {
  70. break
  71. }
  72. }
  73. }
  74. // parse BootCurrent
  75. pf(func(l string) bool {
  76. if current := parseEFIBootMGRBootCurrent(l); current != "" {
  77. mgr.bootCurrent = current
  78. return true
  79. }
  80. return false
  81. })
  82. // parse Timeout second
  83. pf(func(l string) bool {
  84. if timeout := parseEFIBootMGRTimeout(l); timeout != -1 {
  85. mgr.timeout = timeout
  86. return true
  87. }
  88. return false
  89. })
  90. // parse BootOrder
  91. pf(func(l string) bool {
  92. if order := parseEFIBootMGRBootOrder(l); len(order) != 0 {
  93. mgr.bootOrder = order
  94. return true
  95. }
  96. return false
  97. })
  98. // parse BootNext
  99. pf(func(l string) bool {
  100. if next := parseEFIBootMGRBootNext(l); next != "" {
  101. mgr.bootNext = next
  102. return true
  103. }
  104. return false
  105. })
  106. // parse entries
  107. pf(func(l string) bool {
  108. if entry := parseEFIBootMGREntry(l); entry != nil {
  109. mgr.entries[entry.BootNum] = entry
  110. }
  111. return false
  112. })
  113. // finally check
  114. if err := mgr.DataCheck(); err != nil {
  115. return nil, errors.Wrap(err, "Invalid efibootmgr parse")
  116. }
  117. return mgr, nil
  118. }
  119. func (m *BootMgr) DataCheck() error {
  120. if m.bootCurrent == "" {
  121. return errors.Error("BootCurrent is empty")
  122. }
  123. if len(m.bootOrder) == 0 {
  124. return errors.Error("BootOrder length is 0")
  125. }
  126. // check if BootOrder in entries
  127. for _, orderNum := range m.bootOrder {
  128. if _, ok := m.entries[orderNum]; !ok {
  129. return errors.Errorf("Not found BootOrder %s entry", orderNum)
  130. }
  131. }
  132. return nil
  133. }
  134. func parseEFIBootMGRBootCurrent(line string) string {
  135. prefix := "BootCurrent: "
  136. if strings.HasPrefix(line, prefix) {
  137. return strings.Split(line, prefix)[1]
  138. }
  139. return ""
  140. }
  141. func parseEFIBootMGRBootOrder(line string) []string {
  142. prefix := "BootOrder: "
  143. if !strings.HasPrefix(line, prefix) {
  144. return nil
  145. }
  146. orderStr := strings.Split(line, prefix)[1]
  147. return strings.Split(orderStr, ",")
  148. }
  149. func parseEFIBootMGRBootNext(line string) string {
  150. prefix := "BootNext: "
  151. if !strings.HasPrefix(line, prefix) {
  152. return ""
  153. }
  154. return strings.Split(line, prefix)[1]
  155. }
  156. func parseEFIBootMGRTimeout(line string) int {
  157. timeoutRegexp := `^Timeout: (?P<seconds>[0-9]{1,}) seconds`
  158. matches := regutils2.SubGroupMatch(timeoutRegexp, line)
  159. if len(matches) == 0 {
  160. return -1
  161. }
  162. secondStr := matches["seconds"]
  163. second, err := strconv.Atoi(secondStr)
  164. if err != nil {
  165. log.Errorf("parse %s seconds error: %v", secondStr, err)
  166. return -1
  167. }
  168. return second
  169. }
  170. func parseEFIBootMGREntry(line string) *BootEntry {
  171. entryRegexp := `^Boot(?P<num>[0-9a-zA-Z]{4})[^:]+?\s+(?P<description>.*)`
  172. matches := regutils2.SubGroupMatch(entryRegexp, line)
  173. if len(matches) == 0 {
  174. return nil
  175. }
  176. num, ok := matches["num"]
  177. if !ok {
  178. return nil
  179. }
  180. desc, ok := matches["description"]
  181. if !ok {
  182. return nil
  183. }
  184. isActive := false
  185. if strings.Contains(line, "* ") {
  186. isActive = true
  187. }
  188. return &BootEntry{
  189. BootNum: num,
  190. Description: desc,
  191. IsActive: isActive,
  192. }
  193. }
  194. func NewEFIBootMgrFromRemote(cli *ssh.Client, sudo bool) (*BootMgr, error) {
  195. return newEFIBootMgrFromRemote(cli, sudo, true)
  196. }
  197. func newEFIBootMgrFromRemote(cli *ssh.Client, sudo bool, verbose bool) (*BootMgr, error) {
  198. cmd := getEFIBootMgrCmd(sudo)
  199. if verbose {
  200. cmd = fmt.Sprintf("%s -v", cmd)
  201. }
  202. lines, err := cli.RawRun(cmd)
  203. if err != nil {
  204. return nil, errors.Wrapf(err, "Execute command: %s", cmd)
  205. }
  206. return ParseEFIBootMGR(strings.Join(lines, "\n"))
  207. }
  208. func (m *BootMgr) GetCommand(sudo bool) string {
  209. return getEFIBootMgrCmd(sudo)
  210. }
  211. func (m *BootMgr) GetBootCurrent() string {
  212. return m.bootCurrent
  213. }
  214. func (m *BootMgr) GetBootOrder() []string {
  215. return m.bootOrder
  216. }
  217. func (m *BootMgr) GetBootNext() string {
  218. return m.bootNext
  219. }
  220. func (m *BootMgr) GetTimeout() int {
  221. return m.timeout
  222. }
  223. func (m *BootMgr) GetBootEntry(num string) *BootEntry {
  224. return m.entries[num]
  225. }
  226. func (m *BootMgr) GetBootEntryByDesc(desc string) *BootEntry {
  227. for _, entry := range m.entries {
  228. if strings.Contains(entry.Description, desc) {
  229. return entry
  230. }
  231. }
  232. return nil
  233. }
  234. func (m *BootMgr) FindBootOrderPos(num string) int {
  235. return stringArraryFindItemPos(m.bootOrder, num)
  236. }
  237. func stringArraryFindItemPos(items []string, item string) int {
  238. for idx, elem := range items {
  239. if elem == item {
  240. return idx
  241. }
  242. }
  243. return -1
  244. }
  245. func stringArraryMove(items []string, item string, pos int) []string {
  246. origPos := stringArraryFindItemPos(items, item)
  247. if origPos == -1 {
  248. items = append(items, item)
  249. origPos = stringArraryFindItemPos(items, item)
  250. }
  251. for i := origPos; i != pos; {
  252. if i < pos {
  253. // from left to right
  254. tmp := items[i]
  255. items[i] = items[i+1]
  256. items[i+1] = tmp
  257. i++
  258. } else if i > pos {
  259. // from right to left
  260. tmp := items[i]
  261. items[i] = items[i-1]
  262. items[i-1] = tmp
  263. i--
  264. }
  265. }
  266. return items
  267. }
  268. func (m *BootMgr) MoveBootOrder(num string, pos int) *BootMgr {
  269. if entry := m.GetBootEntry(num); entry == nil {
  270. log.Warningf("Not found boot entry by %q", num)
  271. return m
  272. }
  273. m.bootOrder = stringArraryMove(m.bootOrder, num, pos)
  274. return m
  275. }
  276. func getSetBootOrderArgs(bootOrder []string) string {
  277. return strings.Join(bootOrder, ",")
  278. }
  279. func (m *BootMgr) GetSetBootOrderArgs() string {
  280. return getSetBootOrderArgs(m.bootOrder)
  281. }
  282. func RemoteIsUEFIBoot(cli *ssh.Client) (bool, error) {
  283. checkCmd := "test -d /sys/firmware/efi && echo is || echo not"
  284. lines, err := cli.Run(checkCmd)
  285. if err != nil {
  286. return false, err
  287. }
  288. for _, line := range lines {
  289. if strings.Contains(line, "is") {
  290. return true, nil
  291. }
  292. }
  293. return false, nil
  294. }
  295. func convertToEFIBootMgrInfo(info *BootMgr) (*types.EFIBootMgrInfo, error) {
  296. data := &types.EFIBootMgrInfo{
  297. PxeBootNum: info.GetBootCurrent(),
  298. BootOrder: make([]*types.EFIBootEntry, len(info.GetBootOrder())),
  299. }
  300. for idx, orderNum := range info.GetBootOrder() {
  301. entry := info.GetBootEntry(orderNum)
  302. if entry == nil {
  303. return nil, errors.Errorf("Not found boot entry by %q", orderNum)
  304. }
  305. data.BootOrder[idx] = &types.EFIBootEntry{
  306. BootNum: entry.BootNum,
  307. Description: entry.Description,
  308. IsActive: entry.IsActive,
  309. }
  310. }
  311. return data, nil
  312. }
  313. func (mgr *BootMgr) ToEFIBootMgrInfo() (*types.EFIBootMgrInfo, error) {
  314. return convertToEFIBootMgrInfo(mgr)
  315. }
  316. func (mgr *BootMgr) sortEntryByKeyword(keyword string) *BootMgr {
  317. newOrder := []string{}
  318. oldOrder := []string{}
  319. for _, num := range mgr.bootOrder {
  320. entry := mgr.GetBootEntry(num)
  321. if strings.Contains(entry.Description, keyword) {
  322. newOrder = append(newOrder, num)
  323. } else {
  324. oldOrder = append(oldOrder, num)
  325. }
  326. }
  327. newOrder = append(newOrder, oldOrder...)
  328. mgr.bootOrder = newOrder
  329. return mgr
  330. }
  331. func RemoteSetCurrentBootAtFirst(cli *ssh.Client, mgr *BootMgr) error {
  332. curPos := mgr.FindBootOrderPos(mgr.GetBootCurrent())
  333. if curPos == -1 {
  334. return errors.Errorf("Not found BootCurrent position %q", mgr.GetBootCurrent())
  335. }
  336. // move to first
  337. mgr.MoveBootOrder(mgr.GetBootCurrent(), 0)
  338. cmd := fmt.Sprintf("%s -o %s", mgr.GetCommand(false), mgr.GetSetBootOrderArgs())
  339. _, err := cli.Run(cmd)
  340. return err
  341. }
  342. func RemoteSetBootOrder(cli *ssh.Client, order []string) error {
  343. cmd := fmt.Sprintf("%s -o %s", SUDO_EFIBOOTMGR, getSetBootOrderArgs(order))
  344. _, err := cli.RunWithTTY(cmd)
  345. return err
  346. }
  347. func RemoteSetBootOrderByInfo(cli *ssh.Client, entry *types.EFIBootEntry) (*BootMgr, error) {
  348. mgr, err := newEFIBootMgrFromRemote(cli, true, true)
  349. if err != nil {
  350. return nil, err
  351. }
  352. curEntry := mgr.GetBootEntryByDesc(entry.Description)
  353. if curEntry == nil {
  354. return nil, errors.Wrapf(err, "Not found remote boot entry by %q", entry.Description)
  355. }
  356. mgr = mgr.sortEntryByKeyword(curEntry.Description)
  357. return mgr, RemoteSetBootOrder(cli, mgr.GetBootOrder())
  358. }
  359. func RemoteTryToSetPXEBoot(cli *ssh.Client) error {
  360. mgr, err := newEFIBootMgrFromRemote(cli, true, true)
  361. if err != nil {
  362. return err
  363. }
  364. mgr = mgr.sortEntryByKeyword(MAC_KEYWORD)
  365. return RemoteSetBootOrder(cli, mgr.GetBootOrder())
  366. }
  367. func remoteISUEFIBootWrap(cli *ssh.Client, f func(*ssh.Client) error) error {
  368. isUEFI, err := RemoteIsUEFIBoot(cli)
  369. if err != nil {
  370. return errors.Wrap(err, "Check is UEFI boot")
  371. }
  372. if !isUEFI {
  373. return nil
  374. }
  375. return f(cli)
  376. }
  377. func RemoteTryRemoveOSBootEntry(hostCli *ssh.Client) error {
  378. return remoteISUEFIBootWrap(hostCli, remoteTryRemoveOSBootEntry)
  379. }
  380. func remoteTryRemoveOSBootEntry(hostCli *ssh.Client) error {
  381. mgr, err := newEFIBootMgrFromRemote(hostCli, false, true)
  382. if err != nil {
  383. return err
  384. }
  385. // TODO: find other ways to decide whether entry is OS boot
  386. osKeywords := []string{
  387. "linux",
  388. "centos",
  389. "ubuntu",
  390. "windows",
  391. "grub",
  392. }
  393. isOsEntry := func(desc string) bool {
  394. for _, key := range osKeywords {
  395. desc := strings.ToLower(desc)
  396. if strings.Contains(desc, key) {
  397. return true
  398. }
  399. }
  400. return false
  401. }
  402. for _, entry := range mgr.entries {
  403. if !isOsEntry(entry.Description) {
  404. continue
  405. }
  406. // delete entry and remove it from BootOrder
  407. cmd := fmt.Sprintf("%s -b %s -B", mgr.GetCommand(false), entry.BootNum)
  408. if _, err := hostCli.Run(cmd); err != nil {
  409. return errors.Wrapf(err, "remove boot entry: %s", entry.Description)
  410. }
  411. }
  412. return nil
  413. }