kickstart_helper.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  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 guestman
  15. import (
  16. "bufio"
  17. "context"
  18. "fmt"
  19. "io"
  20. "net/http"
  21. "os"
  22. "path"
  23. "path/filepath"
  24. "regexp"
  25. "runtime/debug"
  26. "strings"
  27. "time"
  28. "gopkg.in/yaml.v3"
  29. "yunion.io/x/jsonutils"
  30. "yunion.io/x/log"
  31. "yunion.io/x/pkg/errors"
  32. api "yunion.io/x/onecloud/pkg/apis/compute"
  33. "yunion.io/x/onecloud/pkg/hostman/hostutils"
  34. modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
  35. "yunion.io/x/onecloud/pkg/util/fileutils2"
  36. "yunion.io/x/onecloud/pkg/util/mountutils"
  37. "yunion.io/x/onecloud/pkg/util/procutils"
  38. )
  39. const (
  40. KICKSTART_MONITOR_TIMEOUT = 60 * time.Minute
  41. SERIAL_FILE_CHECK_INTERVAL = 5 * time.Second
  42. KICKSTART_ISO_MOUNT_DIR = "iso-mount"
  43. KICKSTART_ISO_BUILD_DIR = "iso-build"
  44. KICKSTART_ISO_FILENAME = "config.iso"
  45. REDHAT_KICKSTART_ISO_VOLUME_LABEL = "OEMDRV"
  46. UBUNTU_KICKSTART_ISO_VOLUME_LABEL = "CIDATA"
  47. )
  48. var (
  49. kickstartInstallingRegex = regexp.MustCompile(`(?i).*KICKSTART_INSTALLING.*`)
  50. kickstartCompletedRegex = regexp.MustCompile(`(?i).*KICKSTART_COMPLETED.*`)
  51. kickstartFailedRegex = regexp.MustCompile(`(?i).*KICKSTART_FAILED.*`)
  52. )
  53. // ensureKickstartCompletionSignal ensures that the kickstart config contains the completion signal
  54. // 1. If already contains completion signal, return as is
  55. // 1. If no %post section, append one with completion signal
  56. // 2. If %post exists but no %end, append completion signal before end of file
  57. // 3. If both %post and %end exist, insert completion signal before %end
  58. func ensureKickstartCompletionSignal(config string) string {
  59. completionSignals := []string{
  60. "echo \"KICKSTART_COMPLETED\" > /dev/ttyS1",
  61. "echo 'KICKSTART_COMPLETED' > /dev/ttyS1",
  62. "echo KICKSTART_COMPLETED > /dev/ttyS1",
  63. }
  64. for _, signal := range completionSignals {
  65. if strings.Contains(config, signal) {
  66. return config
  67. }
  68. }
  69. const standardCompletionSignal = "echo \"KICKSTART_COMPLETED\" > /dev/ttyS1"
  70. postIdx := strings.Index(config, "%post")
  71. if postIdx == -1 {
  72. return config + fmt.Sprintf("\n\n%%post\n%s\n%%end", standardCompletionSignal)
  73. }
  74. endIdx := strings.Index(config[postIdx:], "%end")
  75. if endIdx == -1 {
  76. return config + fmt.Sprintf("\n%s\n%%end", standardCompletionSignal)
  77. }
  78. // Find line index of %end within %post section
  79. lines := strings.Split(config, "\n")
  80. absoluteEndIdx := postIdx + endIdx
  81. endLineIdx := -1
  82. currentPos := 0
  83. for i, line := range lines {
  84. lineEndPos := currentPos + len(line) + 1
  85. if currentPos <= absoluteEndIdx && absoluteEndIdx < lineEndPos {
  86. if strings.TrimSpace(line) == "%end" {
  87. endLineIdx = i
  88. break
  89. }
  90. }
  91. currentPos = lineEndPos
  92. }
  93. if endLineIdx != -1 {
  94. newLines := make([]string, len(lines)+1)
  95. copy(newLines, lines[:endLineIdx])
  96. newLines[endLineIdx] = standardCompletionSignal
  97. copy(newLines[endLineIdx+1:], lines[endLineIdx:])
  98. return strings.Join(newLines, "\n")
  99. }
  100. return config
  101. }
  102. // ensureAutoinstallCompletionSignal ensures that the autoinstall config contains the completion signal
  103. // 1. If already contains completion signal, return as is
  104. // 2. If no late-commands section, create one with completion signal
  105. // 3. If late-commands exists but no completion signal, append completion signal
  106. func ensureAutoinstallCompletionSignal(config string) string {
  107. const standardCompletionSignal = "echo \"KICKSTART_COMPLETED\" > /dev/ttyS1"
  108. completionSignals := []string{
  109. "echo \"KICKSTART_COMPLETED\" > /dev/ttyS1",
  110. "echo 'KICKSTART_COMPLETED' > /dev/ttyS1",
  111. "echo KICKSTART_COMPLETED > /dev/ttyS1",
  112. }
  113. // Check if completion signal already exists
  114. for _, signal := range completionSignals {
  115. if strings.Contains(config, signal) {
  116. return config
  117. }
  118. }
  119. // Parse YAML using string keys, use yaml.v3 for better compatibility
  120. var yamlData map[string]interface{}
  121. err := yaml.Unmarshal([]byte(config), &yamlData)
  122. if err != nil {
  123. log.Warningf("Failed to parse autoinstall config as YAML: %v\n", err)
  124. return config
  125. }
  126. autoinstall, ok := yamlData["autoinstall"].(map[string]interface{})
  127. if !ok {
  128. log.Warningf("No autoinstall section found in config\n")
  129. return config
  130. }
  131. lateCommands, exists := autoinstall["late-commands"]
  132. if !exists {
  133. autoinstall["late-commands"] = []interface{}{standardCompletionSignal}
  134. } else {
  135. if commands, ok := lateCommands.([]interface{}); ok {
  136. autoinstall["late-commands"] = append(commands, standardCompletionSignal)
  137. } else {
  138. autoinstall["late-commands"] = []interface{}{standardCompletionSignal}
  139. }
  140. }
  141. result, err := yaml.Marshal(yamlData)
  142. if err != nil {
  143. log.Warningf("Failed to marshal autoinstall config as YAML: %v\n", err)
  144. return config
  145. }
  146. // #cloud-config is required at the top if originally present
  147. return "#cloud-config\n" + string(result)
  148. }
  149. // ensureCompletionSignal ensures that the configuration contains the completion signal based on OS type
  150. func ensureCompletionSignal(osType, config string) string {
  151. switch osType {
  152. case "centos", "rhel", "fedora", "openeuler":
  153. return ensureKickstartCompletionSignal(config)
  154. case "ubuntu":
  155. return ensureAutoinstallCompletionSignal(config)
  156. default:
  157. return config // No validation for unsupported OS types
  158. }
  159. }
  160. type SKickstartSerialMonitor struct {
  161. serverId string
  162. serialFilePath string
  163. kickstartTmpDir string
  164. ctx context.Context
  165. cancel context.CancelFunc
  166. }
  167. // NewKickstartSerialMonitor creates a new kickstart serial monitor
  168. func NewKickstartSerialMonitor(s *SKVMGuestInstance) *SKickstartSerialMonitor {
  169. ctx, cancel := context.WithCancel(context.Background())
  170. // Get the server instance to access its homeDir
  171. serialFilePath := path.Join(s.HomeDir(), "kickstart-serial.log")
  172. kickstartTmpDir := s.getKickstartTmpDir()
  173. return &SKickstartSerialMonitor{
  174. serverId: s.GetId(),
  175. serialFilePath: serialFilePath,
  176. kickstartTmpDir: kickstartTmpDir,
  177. ctx: ctx,
  178. cancel: cancel,
  179. }
  180. }
  181. func (m *SKickstartSerialMonitor) GetSerialFilePath() string {
  182. return m.serialFilePath
  183. }
  184. // getKickstartTimeout gets kickstart timeout from kickstart config, defaults to KICKSTART_MONITOR_TIMEOUT
  185. func (m *SKickstartSerialMonitor) getKickstartTimeout() time.Duration {
  186. server, exists := guestManager.GetServer(m.serverId)
  187. if !exists {
  188. return KICKSTART_MONITOR_TIMEOUT
  189. }
  190. kvmGuest, ok := server.(*SKVMGuestInstance)
  191. if !ok {
  192. return KICKSTART_MONITOR_TIMEOUT
  193. }
  194. kickstartConfigStr, exists := kvmGuest.Desc.Metadata[api.VM_METADATA_KICKSTART_CONFIG]
  195. if !exists || kickstartConfigStr == "" {
  196. return KICKSTART_MONITOR_TIMEOUT
  197. }
  198. configObj, err := jsonutils.ParseString(kickstartConfigStr)
  199. if err != nil {
  200. return KICKSTART_MONITOR_TIMEOUT
  201. }
  202. var config api.KickstartConfig
  203. if err := configObj.Unmarshal(&config); err != nil {
  204. return KICKSTART_MONITOR_TIMEOUT
  205. }
  206. if config.TimeoutMinutes <= 0 {
  207. return KICKSTART_MONITOR_TIMEOUT
  208. }
  209. timeout := time.Duration(config.TimeoutMinutes) * time.Minute
  210. log.Debugf("Using kickstart timeout %v for server %s", timeout, m.serverId)
  211. return timeout
  212. }
  213. func (m *SKickstartSerialMonitor) ensureSerialFile() error {
  214. if _, err := os.Stat(m.serialFilePath); os.IsNotExist(err) {
  215. file, err := os.Create(m.serialFilePath)
  216. if err != nil {
  217. return errors.Wrapf(err, "create serial file %s", m.serialFilePath)
  218. }
  219. file.Close()
  220. log.Debugf("Created kickstart serial file %s for server %s", m.serialFilePath, m.serverId)
  221. }
  222. return nil
  223. }
  224. func (m *SKickstartSerialMonitor) monitorSerialFile() {
  225. defer func() {
  226. if r := recover(); r != nil {
  227. log.Errorf("KickstartSerialMonitor monitor %v %s", r, debug.Stack())
  228. }
  229. }()
  230. var lastSize int64 = 0
  231. ticker := time.NewTicker(SERIAL_FILE_CHECK_INTERVAL)
  232. defer ticker.Stop()
  233. for {
  234. select {
  235. case <-m.ctx.Done():
  236. return
  237. case <-ticker.C:
  238. if err := m.scanSerialForStatus(&lastSize); err != nil {
  239. log.Errorf("Failed to scan serial file for status for server %s: %v", m.serverId, err)
  240. }
  241. }
  242. }
  243. }
  244. // scanSerialForStatus scans the serial file for a kickstart status update.
  245. // It reads new content since the last check, parses it for status keywords,
  246. // and triggers status updates and cleanup when a final status is detected.
  247. func (m *SKickstartSerialMonitor) scanSerialForStatus(lastSize *int64) error {
  248. fileInfo, err := os.Stat(m.serialFilePath)
  249. if os.IsNotExist(err) {
  250. return nil
  251. }
  252. if err != nil {
  253. return err
  254. }
  255. currentSize := fileInfo.Size()
  256. if currentSize <= *lastSize {
  257. return nil
  258. }
  259. // Read new content
  260. file, err := os.Open(m.serialFilePath)
  261. if err != nil {
  262. return err
  263. }
  264. defer file.Close()
  265. _, err = file.Seek(*lastSize, 0)
  266. if err != nil {
  267. return err
  268. }
  269. scanner := bufio.NewScanner(file)
  270. for scanner.Scan() {
  271. line := strings.TrimSpace(scanner.Text())
  272. if len(line) == 0 {
  273. continue
  274. }
  275. var status string
  276. var shouldClose bool = false
  277. var matched bool = false
  278. if kickstartInstallingRegex.MatchString(line) {
  279. status = api.VM_KICKSTART_INSTALLING
  280. shouldClose = false
  281. matched = true
  282. } else if kickstartCompletedRegex.MatchString(line) {
  283. status = api.VM_KICKSTART_COMPLETED
  284. shouldClose = true
  285. matched = true
  286. // Unmount ISO and restart VM if kickstart install successfully
  287. if err := m.handleKickstartCompleted(); err != nil {
  288. log.Errorf("Failed to handle kickstart success for server %s: %v", m.serverId, err)
  289. }
  290. } else if kickstartFailedRegex.MatchString(line) {
  291. // TODO: auto retry or alert and stop
  292. status = api.VM_KICKSTART_FAILED
  293. shouldClose = true
  294. matched = true
  295. }
  296. if matched {
  297. log.Infof("Kickstart status update for server %s: %s", m.serverId, status)
  298. if err := m.updateKickstartStatus(status); err != nil {
  299. log.Errorf("Failed to update kickstart status for server %s: %v", m.serverId, err)
  300. } else {
  301. if shouldClose {
  302. m.Close()
  303. return nil
  304. }
  305. }
  306. }
  307. }
  308. *lastSize = currentSize
  309. return scanner.Err()
  310. }
  311. func (m *SKickstartSerialMonitor) Close() error {
  312. if m.cancel != nil {
  313. m.cancel()
  314. }
  315. if m.serialFilePath != "" {
  316. if err := os.Remove(m.serialFilePath); err != nil && !os.IsNotExist(err) {
  317. log.Warningf("Failed to remove serial file %s: %v", m.serialFilePath, err)
  318. }
  319. }
  320. log.Debugf("Kickstart monitor closed for server %s", m.serverId)
  321. return nil
  322. }
  323. // updateKickstartStatus updates the kickstart status via Region API
  324. func (m *SKickstartSerialMonitor) updateKickstartStatus(status string) error {
  325. ctx := context.Background()
  326. session := hostutils.GetComputeSession(ctx)
  327. input := api.ServerUpdateKickstartStatusInput{
  328. Status: status,
  329. }
  330. _, err := modules.Servers.PerformAction(session, m.serverId, "update-kickstart-status", jsonutils.Marshal(input))
  331. if err != nil {
  332. return errors.Wrapf(err, "failed to update kickstart status for server %s", m.serverId)
  333. }
  334. return nil
  335. }
  336. // Start starts the kickstart serial monitor
  337. func (m *SKickstartSerialMonitor) Start() error {
  338. log.Debugf("Starting kickstart monitor for server %s, serial file: %s", m.serverId, m.serialFilePath)
  339. if err := m.ensureSerialFile(); err != nil {
  340. return errors.Wrap(err, "ensure serial file")
  341. }
  342. go m.monitorSerialFile()
  343. // Setup timeout handler
  344. go func() {
  345. timeout := m.getKickstartTimeout()
  346. timer := time.NewTimer(timeout)
  347. defer timer.Stop()
  348. select {
  349. case <-m.ctx.Done():
  350. return
  351. case <-timer.C:
  352. log.Warningf("Kickstart monitor timeout (%v) for server %s, setting status to failed", timeout, m.serverId)
  353. if err := m.updateKickstartStatus(api.VM_KICKSTART_FAILED); err != nil {
  354. log.Errorf("Failed to update kickstart status to failed on timeout for server %s: %v", m.serverId, err)
  355. }
  356. m.Close()
  357. }
  358. }()
  359. return nil
  360. }
  361. // handleKickstartCompleted handles the successful kickstart completion
  362. // It unmounts the ISO image and restarts the VM
  363. func (m *SKickstartSerialMonitor) handleKickstartCompleted() error {
  364. // Clean up kickstart files after successful installation
  365. CleanupKickstartFiles(m.serverId, m.kickstartTmpDir)
  366. log.Debugf("Restarting VM %s after successful kickstart", m.serverId)
  367. if err := m.restartServer(); err != nil {
  368. return errors.Wrapf(err, "failed to restart server %s", m.serverId)
  369. }
  370. return nil
  371. }
  372. // restartServer restarts the server using Region API,
  373. // because the kickstart process requires a fully reboot
  374. // to regenerate qemu parameters
  375. func (m *SKickstartSerialMonitor) restartServer() error {
  376. ctx := context.Background()
  377. session := hostutils.GetComputeSession(ctx)
  378. input := jsonutils.NewDict()
  379. input.Set("is_force", jsonutils.JSONFalse)
  380. _, err := modules.Servers.PerformAction(session, m.serverId, "restart", input)
  381. if err != nil {
  382. return errors.Wrapf(err, "failed to restart server %s via API", m.serverId)
  383. }
  384. log.Infof("Server %s restarted after kickstart completion", m.serverId)
  385. return nil
  386. }
  387. // downloadKickstartConfigFromURL downloads kickstart configuration content from the given URL
  388. func downloadKickstartConfigFromURL(configURL string, osType string) (string, error) {
  389. const downloadTimeout = 10 * time.Second
  390. const maxConfigSize = 64 * 1024
  391. // For Ubuntu systems, append "/user-data" to the URL
  392. if osType == "ubuntu" && !strings.HasSuffix(configURL, "/user-data") {
  393. configURL = configURL + "/user-data"
  394. }
  395. client := &http.Client{Timeout: downloadTimeout}
  396. req, err := http.NewRequest("GET", configURL, nil)
  397. if err != nil {
  398. return "", errors.Wrapf(err, "failed to create download request for %s", configURL)
  399. }
  400. resp, err := client.Do(req)
  401. if err != nil {
  402. return "", errors.Wrapf(err, "failed to download config from %s", configURL)
  403. }
  404. defer resp.Body.Close()
  405. if resp.StatusCode != http.StatusOK {
  406. return "", errors.Errorf("download failed: URL returned status code: %d", resp.StatusCode)
  407. }
  408. limitedReader := io.LimitReader(resp.Body, int64(maxConfigSize)+1)
  409. content, err := io.ReadAll(limitedReader)
  410. if err != nil {
  411. return "", errors.Wrapf(err, "failed to read config content from %s", configURL)
  412. }
  413. if len(content) > maxConfigSize {
  414. return "", errors.Errorf("downloaded content too large: %d bytes, maximum allowed: %d bytes", len(content), maxConfigSize)
  415. }
  416. if len(strings.TrimSpace(string(content))) == 0 {
  417. return "", errors.Error("downloaded config content is empty")
  418. }
  419. log.Debugf("Successfully downloaded kickstart config from %s: %d bytes", configURL, len(content))
  420. return string(content), nil
  421. }
  422. // CreateKickstartConfigISO creates an ISO image containing kickstart configuration files
  423. // For Red Hat systems: creates ks.cfg in a ISO with label 'OEMDRV'
  424. // For Ubuntu systems: creates user-data and meta-data files in a ISO with label 'CIDATA'
  425. // reference: https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/automatically_installing_rhel/starting-kickstart-installations#starting-a-kickstart-installation-automatically-using-a-local-volume
  426. // https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/automatically_installing_rhel/starting-kickstart-installations#starting-a-kickstart-installation-automatically-using-a-local-volume
  427. func CreateKickstartConfigISO(config *api.KickstartConfig, kickStartBaseDir string) (string, error) {
  428. log.Debugf("Creating kickstart ISO to %s with OS type %s", kickStartBaseDir, config.OSType)
  429. if config == nil {
  430. return "", errors.Errorf("kickstart config is nil")
  431. }
  432. // Download config content from URL if necessary
  433. if config.Config == "" && config.ConfigURL != "" {
  434. log.Debugf("Downloading kickstart config from URL: %s", config.ConfigURL)
  435. downloadedContent, err := downloadKickstartConfigFromURL(config.ConfigURL, config.OSType)
  436. if err != nil {
  437. return "", errors.Wrapf(err, "failed to download kickstart config from URL")
  438. }
  439. config.Config = downloadedContent
  440. log.Debugf("Kickstart config downloaded and set, length: %d", len(config.Config))
  441. }
  442. if config.Config == "" {
  443. return "", errors.Errorf("kickstart config content is empty")
  444. }
  445. // Ensure completion signal is present
  446. validatedConfig := ensureCompletionSignal(config.OSType, config.Config)
  447. // Create temporary directory for ISO contents
  448. tmpDir := filepath.Join(kickStartBaseDir, KICKSTART_ISO_BUILD_DIR)
  449. if err := os.MkdirAll(tmpDir, 0755); err != nil {
  450. return "", errors.Wrapf(err, "failed to create temp directory %s", tmpDir)
  451. }
  452. defer func() {
  453. if err := os.RemoveAll(tmpDir); err != nil {
  454. log.Warningf("Failed to cleanup temp directory %s: %v", tmpDir, err)
  455. }
  456. }()
  457. var filePaths []string
  458. var volumeLabel string
  459. switch config.OSType {
  460. case "centos", "rhel", "fedora", "openeuler":
  461. // Create anaconda-ks.cfg for Red Hat systems
  462. ksFilePath := filepath.Join(tmpDir, "anaconda-ks.cfg")
  463. if err := os.WriteFile(ksFilePath, []byte(validatedConfig), 0644); err != nil {
  464. return "", errors.Wrapf(err, "failed to write kickstart file %s", ksFilePath)
  465. }
  466. filePaths = []string{ksFilePath}
  467. volumeLabel = REDHAT_KICKSTART_ISO_VOLUME_LABEL
  468. case "ubuntu":
  469. // Create user-data file for Ubuntu systems
  470. userDataPath := filepath.Join(tmpDir, "user-data")
  471. if err := os.WriteFile(userDataPath, []byte(validatedConfig), 0644); err != nil {
  472. return "", errors.Wrapf(err, "failed to write user-data file %s", userDataPath)
  473. }
  474. // Create empty meta-data file
  475. metaDataPath := filepath.Join(tmpDir, "meta-data")
  476. if err := os.WriteFile(metaDataPath, []byte(""), 0644); err != nil {
  477. return "", errors.Wrapf(err, "failed to write meta-data file %s", metaDataPath)
  478. }
  479. filePaths = []string{userDataPath, metaDataPath}
  480. volumeLabel = UBUNTU_KICKSTART_ISO_VOLUME_LABEL
  481. default:
  482. return "", errors.Errorf("unsupported OS type: %s", config.OSType)
  483. }
  484. // Create ISO using mkisofs
  485. isoPath := filepath.Join(kickStartBaseDir, KICKSTART_ISO_FILENAME)
  486. args := []string{
  487. "-o", isoPath,
  488. "-V", volumeLabel,
  489. "-r",
  490. "-J",
  491. }
  492. args = append(args, filePaths...)
  493. cmd := procutils.NewCommand("mkisofs", args...)
  494. if output, err := cmd.Output(); err != nil {
  495. return "", errors.Wrapf(err, "mkisofs failed: %s", string(output))
  496. }
  497. log.Debugf("Successfully created kickstart ISO: %s", isoPath)
  498. return isoPath, nil
  499. }
  500. // ComputeKickstartKernelInitrdPaths returns absolute kernel and initrd paths by combining
  501. // mountPath with OS-specific relative paths and validating that files exist.
  502. // GetKernelInitrdPaths resolves absolute kernel and initrd paths under a mounted ISO.
  503. func GetKernelInitrdPaths(mountPath, osType string) (string, string, error) {
  504. var kernelRelPath, initrdRelPath string
  505. switch osType {
  506. case "centos", "rhel", "fedora", "openeuler":
  507. kernelRelPath = "images/pxeboot/vmlinuz"
  508. initrdRelPath = "images/pxeboot/initrd.img"
  509. case "ubuntu":
  510. kernelRelPath = "casper/vmlinuz"
  511. initrdRelPath = "casper/initrd"
  512. default:
  513. return "", "", errors.Errorf("unsupported OS type: %s", osType)
  514. }
  515. kernelPath := path.Join(mountPath, kernelRelPath)
  516. initrdPath := path.Join(mountPath, initrdRelPath)
  517. if !fileutils2.Exists(kernelPath) {
  518. return "", "", errors.Errorf("kernel file not found: %s", kernelPath)
  519. }
  520. if !fileutils2.Exists(initrdPath) {
  521. return "", "", errors.Errorf("initrd file not found: %s", initrdPath)
  522. }
  523. return kernelPath, initrdPath, nil
  524. }
  525. // BuildKickstartAppendArgs builds kernel append args for kickstart/autoinstall
  526. // based on OS type and whether a local config ISO is present.
  527. // isoPath non-empty indicates a locally attached config ISO.
  528. func BuildKickstartAppendArgs(config *api.KickstartConfig, isoPath string) string {
  529. if config == nil {
  530. return ""
  531. }
  532. baseArgs := []string{}
  533. var kickstartArgs []string
  534. switch config.OSType {
  535. case "centos", "rhel", "fedora", "openeuler":
  536. if isoPath != "" {
  537. kickstartArgs = append(kickstartArgs, fmt.Sprintf("inst.ks=hd:LABEL=%s:/anaconda-ks.cfg", REDHAT_KICKSTART_ISO_VOLUME_LABEL))
  538. } else if config.ConfigURL != "" {
  539. // Fallback to URL directly if no local ISO available
  540. kickstartArgs = append(kickstartArgs, fmt.Sprintf("inst.ks=%s", config.ConfigURL))
  541. } else {
  542. kickstartArgs = append(kickstartArgs, "inst.ks=cdrom:/anaconda-ks.cfg")
  543. }
  544. case "ubuntu":
  545. if isoPath != "" {
  546. kickstartArgs = append(kickstartArgs, "autoinstall")
  547. } else if config.ConfigURL != "" {
  548. // Fallback to URL directly if no local ISO available
  549. kickstartArgs = append(kickstartArgs,
  550. "autoinstall",
  551. "ip=dhcp",
  552. fmt.Sprintf("ds=nocloud-net;s=%s", config.ConfigURL),
  553. )
  554. } else {
  555. kickstartArgs = append(kickstartArgs, "autoinstall", "ip=dhcp", "ds=nocloud;s=/cdrom/")
  556. }
  557. }
  558. return strings.Join(append(baseArgs, kickstartArgs...), " ")
  559. }
  560. // CleanupKickstartFiles cleans up all kickstart-related temporary files and directories
  561. func CleanupKickstartFiles(serverId, kickstartDir string) {
  562. if serverId == "" {
  563. log.Warningf("Empty serverId provided for kickstart cleanup")
  564. return
  565. }
  566. mountPoint := filepath.Join(kickstartDir, KICKSTART_ISO_MOUNT_DIR)
  567. if fileutils2.Exists(mountPoint) {
  568. log.Infof("Unmounting kickstart ISO at %s for server %s", mountPoint, serverId)
  569. if err := mountutils.Unmount(mountPoint, true); err != nil {
  570. log.Errorf("Failed to unmount kickstart ISO at %s: %v", mountPoint, err)
  571. } else {
  572. log.Debugf("Successfully unmounted kickstart ISO at %s for server %s", mountPoint, serverId)
  573. }
  574. }
  575. if server, exists := guestManager.GetServer(serverId); exists {
  576. if kvmGuest, ok := server.(*SKVMGuestInstance); ok && len(kvmGuest.Desc.Cdroms) > 0 {
  577. for _, cdrom := range kvmGuest.Desc.Cdroms {
  578. if cdrom.Path != "" {
  579. filename := path.Base(cdrom.Path)
  580. if strings.HasPrefix(filename, "kickstart-") {
  581. if err := os.Remove(cdrom.Path); err != nil && !os.IsNotExist(err) {
  582. log.Errorf("Failed to cleanup kickstart ISO %s: %v", cdrom.Path, err)
  583. } else {
  584. log.Debugf("Successfully removed kickstart ISO %s for server %s", cdrom.Path, serverId)
  585. }
  586. break
  587. }
  588. }
  589. }
  590. }
  591. }
  592. if fileutils2.Exists(kickstartDir) {
  593. log.Infof("Removing kickstart directory %s for server %s", kickstartDir, serverId)
  594. if err := os.RemoveAll(kickstartDir); err != nil {
  595. log.Errorf("Failed to remove kickstart directory %s: %v", kickstartDir, err)
  596. } else {
  597. log.Debugf("Successfully removed kickstart directory %s for server %s", kickstartDir, serverId)
  598. }
  599. }
  600. log.Debugf("Kickstart files cleanup completed for server %s", serverId)
  601. }