qemu-kvmhelper.go 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563
  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. "context"
  17. "fmt"
  18. "net"
  19. "os"
  20. "path"
  21. "path/filepath"
  22. "strconv"
  23. "strings"
  24. "time"
  25. "github.com/mdlayher/arp"
  26. "github.com/mdlayher/ethernet"
  27. "github.com/sergi/go-diff/diffmatchpatch"
  28. "yunion.io/x/jsonutils"
  29. "yunion.io/x/log"
  30. "yunion.io/x/pkg/errors"
  31. "yunion.io/x/pkg/utils"
  32. "yunion.io/x/onecloud/pkg/apis"
  33. api "yunion.io/x/onecloud/pkg/apis/compute"
  34. "yunion.io/x/onecloud/pkg/hostman/guestman/desc"
  35. "yunion.io/x/onecloud/pkg/hostman/guestman/qemu"
  36. qemucerts "yunion.io/x/onecloud/pkg/hostman/guestman/qemu/certs"
  37. deployapi "yunion.io/x/onecloud/pkg/hostman/hostdeployer/apis"
  38. "yunion.io/x/onecloud/pkg/hostman/hostdeployer/deployclient"
  39. "yunion.io/x/onecloud/pkg/hostman/hostdeployer/uefi"
  40. "yunion.io/x/onecloud/pkg/hostman/monitor"
  41. "yunion.io/x/onecloud/pkg/hostman/options"
  42. "yunion.io/x/onecloud/pkg/hostman/storageman"
  43. "yunion.io/x/onecloud/pkg/util/fileutils2"
  44. "yunion.io/x/onecloud/pkg/util/mountutils"
  45. "yunion.io/x/onecloud/pkg/util/procutils"
  46. "yunion.io/x/onecloud/pkg/util/qemutils"
  47. )
  48. const (
  49. OS_NAME_LINUX = qemu.OS_NAME_LINUX
  50. OS_NAME_WINDOWS = qemu.OS_NAME_WINDOWS
  51. OS_NAME_MACOS = qemu.OS_NAME_MACOS
  52. OS_NAME_ANDROID = qemu.OS_NAME_ANDROID
  53. OS_NAME_VMWARE = qemu.OS_NAME_VMWARE
  54. OS_NAME_CIRROS = qemu.OS_NAME_CIRROS
  55. OS_NAME_OPENWRT = qemu.OS_NAME_OPENWRT
  56. MODE_READLINE = qemu.MODE_READLINE
  57. MODE_CONTROL = qemu.MODE_CONTROL
  58. DISK_DRIVER_VIRTIO = qemu.DISK_DRIVER_VIRTIO
  59. DISK_DRIVER_SCSI = qemu.DISK_DRIVER_SCSI
  60. DISK_DRIVER_PVSCSI = qemu.DISK_DRIVER_PVSCSI
  61. DISK_DRIVER_IDE = qemu.DISK_DRIVER_IDE
  62. DISK_DRIVER_SATA = qemu.DISK_DRIVER_SATA
  63. )
  64. const guestLauncher = `#!/usr/bin/env python
  65. import sys
  66. import os
  67. import time
  68. import subprocess
  69. import shlex
  70. with open(os.devnull, 'w') as FNULL:
  71. try:
  72. cmd_str = subprocess.check_output(['bash', '%s'], stderr=FNULL).decode('utf-8').strip()
  73. cmd = shlex.split(cmd_str)
  74. except BaseException as e:
  75. sys.stderr.write('%%s' %% e)
  76. sys.exit(1)
  77. statePath = '%s'
  78. if os.path.exists(statePath):
  79. cmd += ['--incoming', 'exec: cat %%s' %% statePath]
  80. elif os.path.exists('%%s/content' %% statePath):
  81. cmd += ['--incoming', 'exec: cat %%s/content' %% statePath]
  82. pid = os.fork()
  83. if pid < 0:
  84. sys.stderr.write('failed fork child process')
  85. sys.exit(1)
  86. if pid > 0:
  87. status_encoded = os.waitpid(pid, 0)[1]
  88. sys.exit((status_encoded>>8) & 0xff)
  89. else:
  90. os.setsid()
  91. os.chdir('/')
  92. pid = os.fork()
  93. if pid < 0:
  94. sys.stderr.write('failed fork child process')
  95. sys.exit(1)
  96. if pid > 0:
  97. sys.stdout.write('%%d' %% pid)
  98. sys.exit(0)
  99. else:
  100. devnull = os.open('/dev/null', os.O_RDWR)
  101. os.dup2(devnull, 0)
  102. os.close(devnull)
  103. logfd = os.open('%s', os.O_RDWR|os.O_CREAT|os.O_APPEND)
  104. os.dup2(logfd, 1)
  105. os.dup2(logfd, 2)
  106. os.write(logfd, str.encode('%%s Run command: %%s\n' %% (time.strftime('%%Y-%%m-%%d %%H:%%M:%%S', time.localtime()), cmd)))
  107. os.close(logfd)
  108. if os.execv(cmd[0], cmd) < 0:
  109. sys.stderr.write('exec error')
  110. sys.exit(1)
  111. `
  112. func (s *SKVMGuestInstance) IsKvmSupport() bool {
  113. return s.manager.GetHost().IsKvmSupport()
  114. }
  115. func (s *SKVMGuestInstance) IsNestedVirt() bool {
  116. return s.manager.GetHost().IsNestedVirtualization()
  117. }
  118. func (s *SKVMGuestInstance) GetKernelVersion() string {
  119. return s.manager.host.GetKernelVersion()
  120. }
  121. func (s *SKVMGuestInstance) HideHypervisor() bool {
  122. if s.IsRunning() && s.IsMonitorAlive() {
  123. cmdline, _ := fileutils2.FileGetContents(path.Join("/proc", strconv.Itoa(s.GetPid()), "cmdline"))
  124. if strings.Contains(cmdline, "hypervisor=off") {
  125. return true
  126. }
  127. }
  128. if s.hasGPU() && s.GetOsName() == OS_NAME_WINDOWS {
  129. return true
  130. }
  131. return false
  132. }
  133. func (s *SKVMGuestInstance) HideKVM() bool {
  134. if s.IsRunning() && s.IsMonitorAlive() {
  135. cmdline, _ := fileutils2.FileGetContents(path.Join("/proc", strconv.Itoa(s.GetPid()), "cmdline"))
  136. if strings.Contains(cmdline, "kvm=off") {
  137. return true
  138. }
  139. }
  140. if s.hasGPU() && s.GetOsName() != OS_NAME_WINDOWS {
  141. return true
  142. }
  143. return false
  144. }
  145. func (s *SKVMGuestInstance) CpuMax() (uint, error) {
  146. cpuMax, ok := s.manager.qemuMachineCpuMax[s.Desc.Machine]
  147. if !ok {
  148. return 0, errors.Errorf("unsupported cpu max for qemu machine: %s", s.Desc.Machine)
  149. }
  150. return cpuMax, nil
  151. }
  152. func (s *SKVMGuestInstance) IsVdiSpice() bool {
  153. return s.Desc.Vdi == "spice"
  154. }
  155. func (s *SKVMGuestInstance) GetOsName() string {
  156. if osName, ok := s.Desc.Metadata["os_name"]; ok {
  157. return osName
  158. }
  159. return OS_NAME_LINUX
  160. }
  161. func (s *SKVMGuestInstance) disableUsbKbd() bool {
  162. return s.Desc.Metadata["disable_usb_kbd"] == "true"
  163. }
  164. func (s *SKVMGuestInstance) getOsDistribution() string {
  165. return s.Desc.Metadata["os_distribution"]
  166. }
  167. func (s *SKVMGuestInstance) getOsVersion() string {
  168. return s.Desc.Metadata["os_version"]
  169. }
  170. func (s *SKVMGuestInstance) getOsCurrentVersion() string {
  171. return s.Desc.Metadata["os_current_version"]
  172. }
  173. func (s *SKVMGuestInstance) pciInitialized() bool {
  174. return len(s.Desc.PCIControllers) > 0
  175. }
  176. func (s *SKVMGuestInstance) hasPcieExtendBus() bool {
  177. return s.Desc.Metadata["__pcie_extend_bus"] == "true"
  178. }
  179. func (s *SKVMGuestInstance) setPcieExtendBus() {
  180. s.Desc.Metadata["__pcie_extend_bus"] = "true"
  181. }
  182. func (s *SKVMGuestInstance) getUsbControllerType() string {
  183. usbContType := s.Desc.Metadata["usb_controller_type"]
  184. if usbContType == "usb-ehci" {
  185. return usbContType
  186. } else {
  187. return "qemu-xhci"
  188. }
  189. }
  190. // is windows prioer to windows server 2003
  191. func (s *SKVMGuestInstance) IsOldWindows() bool {
  192. if s.GetOsName() == OS_NAME_WINDOWS {
  193. cv := s.getOsCurrentVersion()
  194. if len(cv) > 0 {
  195. if len(cv) > 1 && cv[0:2] == "5." {
  196. return true
  197. }
  198. } else {
  199. ver := s.getOsVersion()
  200. if len(ver) > 1 && ver[0:2] == "5." {
  201. return true
  202. }
  203. }
  204. }
  205. return false
  206. }
  207. func (s *SKVMGuestInstance) isWindows10() bool {
  208. if s.GetOsName() == OS_NAME_WINDOWS {
  209. distro := s.getOsDistribution()
  210. if strings.Contains(strings.ToLower(distro), "windows 10") {
  211. return true
  212. }
  213. osVer := s.getOsVersion()
  214. if strings.Contains(strings.ToLower(osVer), "windows 10") {
  215. return true
  216. }
  217. }
  218. return false
  219. }
  220. func (s *SKVMGuestInstance) isMemcleanEnabled() bool {
  221. return s.Desc.Metadata[api.VM_METADATA_ENABLE_MEMCLEAN] == "true"
  222. }
  223. func (s *SKVMGuestInstance) isDisableAutoMergeSnapshots() bool {
  224. return s.Desc.Metadata[api.VM_METADATA_DISABLE_AUTO_MERGE_SNAPSHOT] == "true"
  225. }
  226. func (s *SKVMGuestInstance) getMachine() string {
  227. machine := s.Desc.Machine
  228. if machine == "" {
  229. machine = api.VM_MACHINE_TYPE_PC
  230. }
  231. return machine
  232. }
  233. func (s *SKVMGuestInstance) getBios() string {
  234. bios := s.Desc.Bios
  235. if bios == "" {
  236. bios = api.VM_BOOT_MODE_BIOS
  237. }
  238. return bios
  239. }
  240. func (s *SKVMGuestInstance) isQ35() bool {
  241. return s.getMachine() == api.VM_MACHINE_TYPE_Q35
  242. }
  243. func (s *SKVMGuestInstance) isVirt() bool {
  244. return s.getMachine() == api.VM_MACHINE_TYPE_VIRT
  245. }
  246. func (s *SKVMGuestInstance) isPcie() bool {
  247. return utils.IsInStringArray(s.getMachine(),
  248. []string{api.VM_MACHINE_TYPE_Q35, api.VM_MACHINE_TYPE_VIRT})
  249. }
  250. func (s *SKVMGuestInstance) GetVdiProtocol() string {
  251. vdi := s.Desc.Vdi
  252. if vdi == "" {
  253. vdi = "vnc"
  254. }
  255. return vdi
  256. }
  257. func (s *SKVMGuestInstance) GetPciBus() string {
  258. if s.isQ35() || s.isVirt() {
  259. return "pcie.0"
  260. } else {
  261. return "pci.0"
  262. }
  263. }
  264. func (s *SKVMGuestInstance) disableIsaSerialDev() bool {
  265. return s.Desc.Metadata["disable_isa_serial"] == "true"
  266. }
  267. func (s *SKVMGuestInstance) disablePvpanicDev() bool {
  268. return s.Desc.Metadata["disable_pvpanic"] == "true"
  269. }
  270. func (s *SKVMGuestInstance) enableTpmDev() bool {
  271. return s.Desc.Metadata[api.VM_METADATA_ENABLE_TPM] == "true"
  272. }
  273. func (s *SKVMGuestInstance) getQuorumChildIndex() int64 {
  274. if sidx, ok := s.Desc.Metadata[api.QUORUM_CHILD_INDEX]; ok {
  275. idx, _ := strconv.ParseInt(sidx, 10, 0)
  276. return idx
  277. }
  278. return 0
  279. }
  280. func (s *SKVMGuestInstance) getNicUpScriptPath(nic *desc.SGuestNetwork) string {
  281. dev := s.manager.GetHost().GetBridgeDev(nic.Bridge)
  282. return path.Join(s.HomeDir(), fmt.Sprintf("if-up-%s-%s.sh", dev.Bridge(), nic.Ifname))
  283. }
  284. func (s *SKVMGuestInstance) getNicDownScriptPath(nic *desc.SGuestNetwork) string {
  285. dev := s.manager.GetHost().GetBridgeDev(nic.Bridge)
  286. return path.Join(s.HomeDir(), fmt.Sprintf("if-down-%s-%s.sh", dev.Bridge(), nic.Ifname))
  287. }
  288. func (s *SKVMGuestInstance) generateNicScripts(nic *desc.SGuestNetwork) error {
  289. bridge := nic.Bridge
  290. dev := s.manager.GetHost().GetBridgeDev(bridge)
  291. if dev == nil {
  292. return fmt.Errorf("Can't find bridge %s", bridge)
  293. }
  294. isVolatileHost := s.IsSlave() || s.IsMigratingDestGuest()
  295. if err := dev.GenerateIfupScripts(s.getNicUpScriptPath(nic), nic, isVolatileHost); err != nil {
  296. return errors.Wrap(err, "GenerateIfupScripts")
  297. }
  298. if err := dev.GenerateIfdownScripts(s.getNicDownScriptPath(nic), nic, isVolatileHost); err != nil {
  299. return errors.Wrap(err, "GenerateIfdownScripts")
  300. }
  301. return nil
  302. }
  303. func (s *SKVMGuestInstance) getNicDeviceModel(name string) string {
  304. return qemu.GetNicDeviceModel(name)
  305. }
  306. func (s *SKVMGuestInstance) extraOptions() string {
  307. cmd := " "
  308. for k, v := range s.Desc.ExtraOptions {
  309. switch jsonV := v.(type) {
  310. case *jsonutils.JSONArray:
  311. for i := 0; i < jsonV.Size(); i++ {
  312. vAtI, _ := jsonV.GetAt(i)
  313. vStr, _ := vAtI.GetString()
  314. cmd += fmt.Sprintf(" -%s %s", k, vStr)
  315. }
  316. default:
  317. vstr, _ := v.GetString()
  318. cmd += fmt.Sprintf(" -%s %s", k, vstr)
  319. }
  320. }
  321. return cmd
  322. }
  323. func (s *SKVMGuestInstance) generateStartScript(data *jsonutils.JSONDict) (string, error) {
  324. // initial data
  325. var input = &qemu.GenerateStartOptionsInput{
  326. GuestDesc: s.Desc,
  327. OsName: s.GetOsName(),
  328. OVNIntegrationBridge: options.HostOptions.OvnIntegrationBridge,
  329. HomeDir: s.HomeDir(),
  330. HugepagesEnabled: s.manager.host.IsHugepagesEnabled(),
  331. EnableMemfd: s.isMemcleanEnabled(),
  332. EnableTpm: s.enableTpmDev(),
  333. PidFilePath: s.GetPidFilePath(),
  334. }
  335. if data.Contains("encrypt_key") {
  336. key, _ := data.GetString("encrypt_key")
  337. if err := s.saveEncryptKeyFile(key); err != nil {
  338. return "", errors.Wrap(err, "save encrypt key file")
  339. }
  340. input.EncryptKeyPath = s.getEncryptKeyPath()
  341. }
  342. cmd := ""
  343. // inject vncPort
  344. vncPort, _ := data.Int("vnc_port")
  345. input.VNCPort = uint(vncPort)
  346. // inject qemu version and arch
  347. qemuVersion := options.HostOptions.DefaultQemuVersion
  348. if data.Contains("qemu_version") {
  349. qemuVersion, _ = data.GetString("qemu_version")
  350. }
  351. if qemuVersion == "latest" {
  352. qemuVersion = ""
  353. }
  354. input.QemuVersion = qemu.Version(qemuVersion)
  355. // inject qemu arch
  356. if s.manager.host.IsAarch64() {
  357. input.QemuArch = qemu.Arch_aarch64
  358. } else if s.manager.host.IsRiscv64() {
  359. input.QemuArch = qemu.Arch_riscv64
  360. } else {
  361. input.QemuArch = qemu.Arch_x86_64
  362. }
  363. for _, nic := range s.Desc.Nics {
  364. if nic.Driver == api.NETWORK_DRIVER_VFIO {
  365. continue
  366. }
  367. downscript := s.getNicDownScriptPath(nic)
  368. cmd += fmt.Sprintf("%s %s\n", downscript, nic.Ifname)
  369. }
  370. traffic, err := guestManager.GetGuestTrafficRecord(s.Id)
  371. if err != nil {
  372. return "", errors.Wrap(err, "get guest traffic record")
  373. }
  374. input.NicTraffics = traffic
  375. if input.HugepagesEnabled {
  376. cmd += fmt.Sprintf("mkdir -p /dev/hugepages/%s\n", s.Desc.Uuid)
  377. cmd += fmt.Sprintf("mount -t hugetlbfs -o pagesize=%dK,size=%dM hugetlbfs-%s /dev/hugepages/%s\n",
  378. s.manager.host.HugepageSizeKb(), s.Desc.Mem, s.Desc.Uuid, s.Desc.Uuid)
  379. }
  380. cmd += "sleep 1\n"
  381. cmd += fmt.Sprintf("echo %d > %s\n", input.VNCPort, s.GetVncFilePath())
  382. diskScripts, err := s.generateDiskSetupScripts(s.Desc.Disks)
  383. if err != nil {
  384. return "", errors.Wrap(err, "generateDiskSetupScripts")
  385. }
  386. cmd += diskScripts
  387. sriovInitScripts, err := s.generateSRIOVInitScripts()
  388. if err != nil {
  389. return "", errors.Wrap(err, "generateSRIOVInitScripts")
  390. }
  391. cmd += sriovInitScripts
  392. // cmd += fmt.Sprintf("STATE_FILE=`ls -d %s* | head -n 1`\n", s.getStateFilePathRootPrefix())
  393. cmd += fmt.Sprintf("PID_FILE=%s\n", input.PidFilePath)
  394. var qemuCmd = qemutils.GetQemu(string(input.QemuVersion))
  395. if len(qemuCmd) == 0 {
  396. qemuCmd = qemutils.GetQemu("")
  397. }
  398. cmd += fmt.Sprintf("DEFAULT_QEMU_CMD='%s'\n", qemuCmd)
  399. /*
  400. * cmd += "if [ -n \"$STATE_FILE\" ]; then\n"
  401. * cmd += " QEMU_VER=`echo $STATE_FILE" +
  402. * ` | grep -o '_[[:digit:]]\+\.[[:digit:]]\+.*'` + "`\n"
  403. * cmd += " QEMU_CMD=\"qemu-system-x86_64\"\n"
  404. * cmd += " QEMU_LOCAL_PATH=\"/usr/local/bin/$QEMU_CMD\"\n"
  405. * cmd += " QEMU_LOCAL_PATH_VER=\"/usr/local/qemu-$QEMU_VER/bin/$QEMU_CMD\"\n"
  406. * cmd += " QEMU_BIN_PATH=\"/usr/bin/$QEMU_CMD\"\n"
  407. * cmd += " if [ -f \"$QEMU_LOCAL_PATH_VER\" ]; then\n"
  408. * cmd += " QEMU_CMD=$QEMU_LOCAL_PATH_VER\n"
  409. * cmd += " elif [ -f \"$QEMU_LOCAL_PATH\" ]; then\n"
  410. * cmd += " QEMU_CMD=$QEMU_LOCAL_PATH\n"
  411. * cmd += " elif [ -f \"$QEMU_BIN_PATH\" ]; then\n"
  412. * cmd += " QEMU_CMD=$QEMU_BIN_PATH\n"
  413. * cmd += " fi\n"
  414. * cmd += "else\n"
  415. * cmd += " QEMU_CMD=$DEFAULT_QEMU_CMD\n"
  416. * cmd += "fi\n"
  417. */
  418. cmd += "QEMU_CMD=$DEFAULT_QEMU_CMD\n"
  419. if s.IsKvmSupport() && !options.HostOptions.DisableKVM {
  420. cmd += "QEMU_CMD_KVM_ARG=-enable-kvm\n"
  421. } else if utils.IsInStringArray(s.manager.host.GetCpuArchitecture(), apis.ARCH_X86) {
  422. // -no-kvm仅x86适用,且将在qemu 5.2之后移除
  423. // https://gitlab.com/qemu-project/qemu/-/blob/master/docs/about/removed-features.rst
  424. cmd += "QEMU_CMD_KVM_ARG=-no-kvm\n"
  425. } else {
  426. cmd += "QEMU_CMD_KVM_ARG=\n"
  427. }
  428. // cmd += "fi\n"
  429. cmd += `
  430. function nic_speed() {
  431. $QEMU_CMD $QEMU_CMD_KVM_ARG -device virtio-net-pci,help 2>&1 | grep -q "\<speed="
  432. if [ "$?" -eq "0" ]; then
  433. echo ",speed=$1"
  434. fi
  435. }
  436. function nic_mtu() {
  437. local bridge="$1"; shift
  438. $QEMU_CMD $QEMU_CMD_KVM_ARG -device virtio-net-pci,help 2>&1 | grep -q '\<host_mtu='
  439. if [ "$?" -eq "0" ]; then
  440. local origmtu="$(<"/sys/class/net/$bridge/mtu")"
  441. if [ -n "$origmtu" -a "$origmtu" -gt 576 ]; then
  442. echo ",host_mtu=$(($origmtu - ` + api.VpcOvnEncapCostStr() + `))"
  443. fi
  444. fi
  445. }
  446. `
  447. if input.EnableTpm {
  448. input.OVMFPath = options.HostOptions.SecbootOvmfPath
  449. input.OVMFVarsPath = options.HostOptions.SecbootOvmfVarsPath
  450. cmd += `
  451. function start_swtpm() {
  452. local swtpm_binary=$1
  453. local swtpm_dir=$2
  454. local swtpm_socket=$swtpm_dir/swtpm.sock
  455. local swtpm_log=$swtpm_dir/swtpm.log
  456. local swtpm_pid=$swtpm_dir/swtpm.pid
  457. if [ -f "$swtpm_pid" ] && ps -p $(cat "$swtpm_pid") >/dev/null 2>&1; then
  458. return 0
  459. fi
  460. mkdir -p $swtpm_dir
  461. $swtpm_binary socket --tpmstate dir=$swtpm_dir --ctrl type=unixio,path=$swtpm_socket --log file=$swtpm_log,level=20 --pid file=$swtpm_pid --tpm2 -d
  462. }
  463. `
  464. cmd += fmt.Sprintf("start_swtpm %s %s\n", options.HostOptions.BinarySwtpmPath, s.getSwtpmDirPath())
  465. }
  466. // Generate Start VM script
  467. cmd += `CMD="$QEMU_CMD $QEMU_CMD_KVM_ARG`
  468. if options.HostOptions.EnableQemuDebugLog {
  469. input.EnableLog = true
  470. }
  471. // inject monitor
  472. input.HMPMonitor = &qemu.Monitor{
  473. Id: "hmqmon",
  474. Port: uint(s.GetHmpMonitorPort(int(input.VNCPort))),
  475. Mode: MODE_READLINE,
  476. }
  477. input.QMPMonitor = &qemu.Monitor{
  478. Id: "qmqmon",
  479. Port: uint(s.GetQmpMonitorPort(int(input.VNCPort))),
  480. Mode: MODE_CONTROL,
  481. }
  482. input.EnableUUID = options.HostOptions.EnableVmUuid
  483. if s.Desc.Bios == qemu.BIOS_UEFI {
  484. if len(input.OVMFPath) == 0 {
  485. input.OVMFPath, input.OVMFVarsPath = s.getOvmfVarsSourcePath()
  486. }
  487. }
  488. // inject usb devices
  489. if !input.QemuArch.IsX86() {
  490. input.Devices = append(input.Devices,
  491. fmt.Sprintf("usb-tablet,id=input0,bus=%s.0,port=1", s.Desc.Usb.Id),
  492. fmt.Sprintf("usb-kbd,id=input1,bus=%s.0,port=2", s.Desc.Usb.Id),
  493. )
  494. // "qemu-xhci,p2=8,p3=8,id=usb1",
  495. // "usb-tablet,id=input0,bus=usb1.0,port=1",
  496. // "usb-kbd,id=input1,bus=usb1.0,port=2",
  497. // "virtio-gpu-pci,id=video1,max_outputs=1",
  498. } else {
  499. if !utils.IsInStringArray(s.getOsDistribution(), []string{OS_NAME_OPENWRT, OS_NAME_CIRROS}) &&
  500. !s.IsOldWindows() && !s.isWindows10() &&
  501. !s.disableUsbKbd() {
  502. input.Devices = append(input.Devices, "usb-kbd")
  503. }
  504. if input.OsName == OS_NAME_ANDROID {
  505. input.Devices = append(input.Devices, "usb-mouse")
  506. } else if !s.IsOldWindows() {
  507. input.Devices = append(input.Devices, "usb-tablet")
  508. }
  509. }
  510. // inject spice and vnc display
  511. input.IsVdiSpice = s.IsVdiSpice()
  512. input.SpicePort = uint(5900 + vncPort)
  513. input.VNCPassword = options.HostOptions.SetVncPassword
  514. input.IsKVMSupport = s.IsKvmSupport()
  515. input.ExtraOptions = append(input.ExtraOptions, s.extraOptions())
  516. if jsonutils.QueryBoolean(data, "need_migrate", false) {
  517. input.NeedMigrate = true
  518. input.LiveMigratePort = uint(*s.LiveMigrateDestPort)
  519. if jsonutils.QueryBoolean(data, "live_migrate_use_tls", false) {
  520. s.LiveMigrateUseTls = true
  521. input.LiveMigrateUseTLS = true
  522. } else {
  523. s.LiveMigrateUseTls = false
  524. }
  525. } else if s.Desc.IsSlave {
  526. log.Infof("backup guest with dest port %v", s.LiveMigrateDestPort)
  527. input.LiveMigratePort = uint(*s.LiveMigrateDestPort)
  528. }
  529. if s.Desc.IsSlave && !jsonutils.QueryBoolean(data, "block_ready", false) {
  530. diskUri, err := data.GetString("disk_uri")
  531. if err != nil {
  532. return "", errors.Wrap(err, "guest start missing disk uri")
  533. }
  534. if err := s.slaveDiskPrepare(input, diskUri); err != nil {
  535. return "", err
  536. }
  537. }
  538. // set rescue flag to input
  539. if s.Desc.LightMode {
  540. input.RescueInitrdPath = s.getRescueInitrdPath()
  541. input.RescueKernelPath = s.getRescueKernelPath()
  542. }
  543. // check if kickstart is needed for KVM guests
  544. if err := s.configureKickstartBoot(input); err != nil {
  545. return "", errors.Wrap(err, "handle kickstart mount")
  546. }
  547. qemuOpts, err := qemu.GenerateStartOptions(input)
  548. if err != nil {
  549. return "", errors.Wrap(err, "GenerateStartCommand")
  550. }
  551. cmd = fmt.Sprintf("%s %s", cmd, qemuOpts)
  552. cmd += "\"\n\n"
  553. cmd += "echo $CMD\n"
  554. return cmd, nil
  555. }
  556. // shouldUseKickstart 判断是否需要启用kickstart自动化安装
  557. // 启动kickstart的条件:1. 虚拟机处于KVM虚拟化环境;2. 存在kickstart配置且未禁用;3. kickstart未完成
  558. func (s *SKVMGuestInstance) shouldUseKickstart() bool {
  559. // 只在KVM虚拟化环境下处理kickstart
  560. if s.Desc.Hypervisor != api.HYPERVISOR_KVM {
  561. return false
  562. }
  563. kickstartCompleted, completedExists := s.Desc.Metadata[api.VM_METADATA_KICKSTART_COMPLETED_FLAG]
  564. if completedExists && kickstartCompleted == "true" {
  565. log.Debugf("Kickstart already completed for VM %s, skipping kickstart boot", s.Id)
  566. return false
  567. }
  568. // 检查是否存在kickstart配置
  569. kickstartConfigStr, configExists := s.Desc.Metadata[api.VM_METADATA_KICKSTART_CONFIG]
  570. if !configExists || kickstartConfigStr == "" {
  571. return false
  572. } else {
  573. kickstartConfigJson, err := jsonutils.ParseString(kickstartConfigStr)
  574. if err != nil {
  575. log.Errorf("Failed to parse kickstart config for VM %s: %v", s.Id, err)
  576. return false
  577. }
  578. kickstartConfig := &api.KickstartConfig{}
  579. if err := kickstartConfigJson.Unmarshal(kickstartConfig); err != nil {
  580. log.Errorf("Failed to unmarshal kickstart config for VM %s: %v", s.Id, err)
  581. return false
  582. }
  583. if kickstartConfig.Enabled != nil && !*kickstartConfig.Enabled {
  584. log.Debugf("Kickstart is disabled in config for VM %s, skipping kickstart boot", s.Id)
  585. return false
  586. }
  587. }
  588. log.Debugf("VM %s needs kickstart: config exists and not completed yet", s.Id)
  589. return true
  590. }
  591. // configureKickstartBoot 配置 kickstart 自动化安装的启动流程
  592. // 1. 挂载安装 ISO,获取内核和 initrd 路径
  593. // 2. 生成内核启动参数
  594. // 3. 创建 kickstart 监控器
  595. // 4. 如果包含完整配置,生成 kickstart 配置 ISO 并添加为 CDROM 设备
  596. func (s *SKVMGuestInstance) configureKickstartBoot(input *qemu.GenerateStartOptionsInput) error {
  597. if !s.shouldUseKickstart() {
  598. return nil
  599. }
  600. log.Debugf("Enabling kickstart boot for VM %s", s.Id)
  601. kickstartConfigStr := s.Desc.Metadata[api.VM_METADATA_KICKSTART_CONFIG]
  602. kickstartConfigJson, err := jsonutils.ParseString(kickstartConfigStr)
  603. if err != nil {
  604. return errors.Wrap(err, "parse kickstart config")
  605. }
  606. kickstartConfig := &api.KickstartConfig{}
  607. if err := kickstartConfigJson.Unmarshal(kickstartConfig); err != nil {
  608. return errors.Wrap(err, "unmarshal kickstart config")
  609. }
  610. // Find ISO file for kickstart installation from CDROM devices
  611. var isoPath string
  612. if len(s.Desc.Cdroms) > 0 {
  613. for _, cdrom := range s.Desc.Cdroms {
  614. if cdrom.Path != "" {
  615. isoPath = cdrom.Path
  616. break
  617. }
  618. }
  619. }
  620. if isoPath == "" {
  621. log.Warningf("no ISO path found for kickstart, skip")
  622. return nil
  623. }
  624. kickstartDir := s.getKickstartTmpDir()
  625. mountPoint := filepath.Join(kickstartDir, KICKSTART_ISO_MOUNT_DIR)
  626. // Check if mount point already exists and is mounted
  627. if fileutils2.Exists(mountPoint) {
  628. mountFile := "/proc/mounts"
  629. if data, err := os.ReadFile(mountFile); err == nil {
  630. lines := strings.Split(string(data), "\n")
  631. mounted := false
  632. for _, line := range lines {
  633. parts := strings.Split(line, " ")
  634. if len(parts) >= 2 && parts[1] == mountPoint {
  635. mounted = true
  636. break
  637. }
  638. }
  639. if mounted {
  640. log.Debugf("Reusing existing kickstart ISO mount at %s for guest %s", mountPoint, s.GetName())
  641. } else {
  642. os.RemoveAll(mountPoint)
  643. if err := os.MkdirAll(mountPoint, 0755); err != nil {
  644. return errors.Wrap(err, "create mount point")
  645. }
  646. if err := mountutils.MountWithParams(isoPath, mountPoint, "iso9660", []string{"-o", "loop,ro"}); err != nil {
  647. os.RemoveAll(mountPoint)
  648. return errors.Wrapf(err, "mount ISO %s to %s", isoPath, mountPoint)
  649. }
  650. log.Debugf("Successfully mounted kickstart ISO %s to %s for guest %s", isoPath, mountPoint, s.GetName())
  651. }
  652. }
  653. } else {
  654. if err := os.MkdirAll(mountPoint, 0755); err != nil {
  655. return errors.Wrap(err, "create mount point")
  656. }
  657. if err := mountutils.MountWithParams(isoPath, mountPoint, "iso9660", []string{"-o", "loop,ro"}); err != nil {
  658. os.RemoveAll(mountPoint)
  659. return errors.Wrapf(err, "mount ISO %s to %s", isoPath, mountPoint)
  660. }
  661. log.Debugf("Successfully mounted kickstart ISO %s to %s for guest %s", isoPath, mountPoint, s.GetName())
  662. }
  663. mountPath := mountPoint
  664. // get kernel and initrd path from mounted ISO
  665. kernelPath, initrdPath, err := GetKernelInitrdPaths(mountPath, kickstartConfig.OSType)
  666. if err != nil {
  667. return errors.Wrap(err, "get kickstart kernel paths")
  668. }
  669. // Copy kernel and initrd files to local directory and unmount ISO
  670. kernelCopyDir := filepath.Join(kickstartDir, "bootfiles")
  671. if err := os.MkdirAll(kernelCopyDir, 0755); err != nil {
  672. return errors.Wrap(err, "create kernel copy directory")
  673. }
  674. kernelFileName := filepath.Base(kernelPath)
  675. initrdFileName := filepath.Base(initrdPath)
  676. copiedKernelPath := filepath.Join(kernelCopyDir, kernelFileName)
  677. copiedInitrdPath := filepath.Join(kernelCopyDir, initrdFileName)
  678. if err := procutils.NewCommand("cp", kernelPath, copiedKernelPath).Run(); err != nil {
  679. return errors.Wrapf(err, "copy kernel from %s to %s", kernelPath, copiedKernelPath)
  680. }
  681. log.Debugf("Successfully copied kernel from %s to %s for guest %s", kernelPath, copiedKernelPath, s.GetName())
  682. if err := procutils.NewCommand("cp", initrdPath, copiedInitrdPath).Run(); err != nil {
  683. return errors.Wrapf(err, "copy initrd from %s to %s", initrdPath, copiedInitrdPath)
  684. }
  685. log.Debugf("Successfully copied initrd from %s to %s for guest %s", initrdPath, copiedInitrdPath, s.GetName())
  686. // Unmount ISO after copying files
  687. log.Infof("Unmounting kickstart ISO at %s after copying kernel and initrd for guest %s", mountPoint, s.GetName())
  688. if err := mountutils.Unmount(mountPoint, true); err != nil {
  689. log.Warningf("Failed to unmount kickstart ISO at %s: %v", mountPoint, err)
  690. } else {
  691. log.Debugf("Successfully unmounted kickstart ISO at %s for guest %s", mountPoint, s.GetName())
  692. // Remove mount point directory after unmounting
  693. if err := os.RemoveAll(mountPoint); err != nil {
  694. log.Warningf("Failed to remove mount point directory %s: %v", mountPoint, err)
  695. }
  696. }
  697. // Use copied file paths instead of mounted paths
  698. kernelPath = copiedKernelPath
  699. initrdPath = copiedInitrdPath
  700. var kickstartConfigIsoPath string
  701. log.Debugf("Kickstart config for guest %s: Config length=%d, ConfigURL=%s",
  702. s.GetName(), len(kickstartConfig.Config), kickstartConfig.ConfigURL)
  703. isoPath, err = CreateKickstartConfigISO(kickstartConfig, s.getKickstartTmpDir())
  704. if err != nil {
  705. log.Errorf("Failed to create kickstart config ISO for guest %s: %v, falling back to URL/cdrom method", s.GetName(), err)
  706. } else {
  707. kickstartConfigIsoPath = isoPath
  708. log.Debugf("Successfully created kickstart ISO for guest %s: %s", s.GetName(), isoPath)
  709. }
  710. kernelArgs := BuildKickstartAppendArgs(kickstartConfig, kickstartConfigIsoPath)
  711. log.Debugf("Generated kickstart kernel args for guest %s: %s", s.GetName(), kernelArgs)
  712. // Create kickstart serial monitor for status monitoring
  713. kickstartMonitor := NewKickstartSerialMonitor(s)
  714. serialFilePath := kickstartMonitor.GetSerialFilePath()
  715. input.KickstartBoot = &qemu.KickstartBootInfo{
  716. Config: kickstartConfig,
  717. MountPath: mountPath,
  718. KernelPath: kernelPath,
  719. InitrdPath: initrdPath,
  720. KernelArgs: kernelArgs,
  721. SerialFilePath: serialFilePath,
  722. ConfigIsoPath: kickstartConfigIsoPath,
  723. }
  724. // Add kickstart config ISO as additional CDROM device if created
  725. if kickstartConfigIsoPath != "" {
  726. if err := s.attachKickstartISO(kickstartConfigIsoPath); err != nil {
  727. log.Warningf("Failed to attach kickstart config ISO %s: %v", kickstartConfigIsoPath, err)
  728. }
  729. }
  730. s.kickstartMonitor = kickstartMonitor
  731. log.Debugf("Kickstart boot configured for guest %s: kernel=%s, initrd=%s, args=%s, isoPath=%s",
  732. s.GetName(), kernelPath, initrdPath, kernelArgs, kickstartConfigIsoPath)
  733. return nil
  734. }
  735. func (s *SKVMGuestInstance) getRescueInitrdPath() string {
  736. return path.Join(s.GetRescueDirPath(), api.GUEST_RESCUE_INITRAMFS)
  737. }
  738. func (s *SKVMGuestInstance) getRescueKernelPath() string {
  739. return path.Join(s.GetRescueDirPath(), api.GUEST_RESCUE_KERNEL)
  740. }
  741. func (s *SKVMGuestInstance) slaveDiskPrepare(input *qemu.GenerateStartOptionsInput, diskUri string) error {
  742. for i := 0; i < len(input.GuestDesc.Disks); i++ {
  743. diskPath := input.GuestDesc.Disks[i].Path
  744. d, err := storageman.GetManager().GetDiskByPath(diskPath)
  745. if err != nil {
  746. return errors.Wrapf(err, "GetDiskByPath(%s)", diskPath)
  747. }
  748. err = d.RebuildSlaveDisk(diskUri)
  749. if err != nil {
  750. return errors.Wrap(err, "RebuildSlaveDisk")
  751. }
  752. }
  753. return nil
  754. }
  755. func (s *SKVMGuestInstance) parseCmdline(input string) (*qemutils.Cmdline, []qemutils.Option, error) {
  756. cl, err := qemutils.NewCmdline(input)
  757. if err != nil {
  758. return nil, nil, errors.Wrapf(err, "NewCmdline %q", input)
  759. }
  760. filterOpts := make([]qemutils.Option, 0)
  761. // filter migrate and other option include dynamic port
  762. cl.FilterOption(func(o qemutils.Option) bool {
  763. switch o.Key {
  764. case "incoming":
  765. if strings.HasPrefix(o.Value, "tcp:") || strings.HasPrefix(o.Value, "defer") {
  766. filterOpts = append(filterOpts, o)
  767. return true
  768. }
  769. case "vnc", "spice", "daemonize":
  770. filterOpts = append(filterOpts, o)
  771. return true
  772. case "chardev":
  773. valsMatch := []string{
  774. "socket,id=hmqmondev",
  775. "socket,id=hmpmondev",
  776. "socket,id=qmqmondev",
  777. "socket,id=qmpmondev",
  778. }
  779. for _, valM := range valsMatch {
  780. if strings.HasPrefix(o.Value, valM) {
  781. filterOpts = append(filterOpts, o)
  782. return true
  783. }
  784. }
  785. }
  786. return false
  787. })
  788. return cl, filterOpts, nil
  789. }
  790. func (s *SKVMGuestInstance) _unifyMigrateQemuCmdline(cur string, src string) string {
  791. dmp := diffmatchpatch.New()
  792. diffs := dmp.DiffMain(cur, src, false)
  793. log.Debugf("unify migrate qemu cmdline diffs: %s", jsonutils.Marshal(diffs).PrettyString())
  794. // make patch
  795. patch := dmp.PatchMake(cur, diffs)
  796. // apply patch
  797. newStr, _ := dmp.PatchApply(patch, cur)
  798. return newStr
  799. }
  800. func (s *SKVMGuestInstance) unifyMigrateQemuCmdline(cur string, src string) (string, error) {
  801. curCl, curFilterOpts, err := s.parseCmdline(cur)
  802. if err != nil {
  803. return "", errors.Wrapf(err, "parseCmdline current %q", cur)
  804. }
  805. srcCl, _, err := s.parseCmdline(src)
  806. if err != nil {
  807. return "", errors.Wrapf(err, "parseCmdline source %q", src)
  808. }
  809. unifyStr := s._unifyMigrateQemuCmdline(curCl.ToString(), srcCl.ToString())
  810. unifyCl, _, err := s.parseCmdline(unifyStr)
  811. if err != nil {
  812. return "", errors.Wrapf(err, "parseCmdline unitfy %q", unifyStr)
  813. }
  814. unifyCl.AddOption(curFilterOpts...)
  815. return unifyCl.ToString(), nil
  816. }
  817. func (s *SKVMGuestInstance) generateStopScript(data *jsonutils.JSONDict) string {
  818. var (
  819. uuid = s.Desc.Uuid
  820. nics = s.Desc.Nics
  821. )
  822. cmd := ""
  823. cmd += fmt.Sprintf("VNC_FILE=%s\n", s.GetVncFilePath())
  824. cmd += fmt.Sprintf("PID_FILE=%s\n", s.GetPidFilePath())
  825. cmd += "if [ \"$1\" != \"--force\" ] && [ -f $VNC_FILE ]; then\n"
  826. cmd += " VNC=`cat $VNC_FILE`\n"
  827. // TODO, replace with qmp monitor
  828. cmd += fmt.Sprintf(" MON=$(($VNC + %d))\n", MONITOR_PORT_BASE)
  829. cmd += " echo quit | nc -w 1 127.0.0.1 $MON > /dev/null\n"
  830. cmd += " sleep 1\n"
  831. cmd += " echo \"Remove VNC $VNC_FILE\"\n"
  832. cmd += " rm -f $VNC_FILE\n"
  833. cmd += "fi\n"
  834. cmd += "if [ -f $PID_FILE ]; then\n"
  835. cmd += " PID=`cat $PID_FILE`\n"
  836. cmd += " ps -p $PID > /dev/null\n"
  837. cmd += " if [ $? -eq 0 ]; then\n"
  838. cmd += " echo \"Kill process $PID\"\n"
  839. cmd += " kill -9 $PID > /dev/null 2>&1\n"
  840. cmd += " fi\n"
  841. cmd += " echo \"Remove PID $PID_FILE\"\n"
  842. cmd += " rm -f $PID_FILE\n"
  843. cmd += "fi\n"
  844. cmd += fmt.Sprintf("for d in $(ls -d /dev/hugepages/%s*)\n", uuid)
  845. cmd += "do\n"
  846. cmd += " if [ -d $d ]; then\n"
  847. cmd += " umount $d\n"
  848. cmd += " rm -rf $d\n"
  849. cmd += " fi\n"
  850. cmd += "done\n"
  851. if s.enableTpmDev() {
  852. // stop swtpm
  853. cmd += fmt.Sprintf("SWTPM_PID_FILE=%s\n", s.getSwtpmPidPath())
  854. cmd += "if [ -f $SWTPM_PID_FILE ]; then\n"
  855. cmd += " SWTPM_PID=`cat $SWTPM_PID_FILE`\n"
  856. cmd += " ps -p $SWTPM_PID > /dev/null\n"
  857. cmd += " if [ $? -eq 0 ]; then\n"
  858. cmd += " echo \"Kill swtpm process $SWTPM_PID\"\n"
  859. cmd += " kill -9 $SWTPM_PID > /dev/null 2>&1\n"
  860. cmd += " fi\n"
  861. cmd += " echo \"Remove swtpm PID $SWTPM_PID_FILE\"\n"
  862. cmd += " rm -f $SWTPM_PID_FILE\n"
  863. cmd += "fi\n"
  864. }
  865. for _, nic := range nics {
  866. if nic.Driver == api.NETWORK_DRIVER_VFIO {
  867. dev, _ := s.GetSriovDeviceByNetworkIndex(nic.Index)
  868. if dev != nil && dev.GetOvsOffloadInterfaceName() == "" {
  869. continue
  870. }
  871. }
  872. downscript := s.getNicDownScriptPath(nic)
  873. cmd += fmt.Sprintf("%s %s\n", downscript, nic.Ifname)
  874. }
  875. return cmd
  876. }
  877. func (s *SKVMGuestInstance) presendArpForNic(nic *desc.SGuestNetwork) {
  878. ifi, err := net.InterfaceByName(nic.Ifname)
  879. if err != nil {
  880. log.Errorf("InterfaceByName error %s", nic.Ifname)
  881. return
  882. }
  883. cli, err := arp.Dial(ifi)
  884. if err != nil {
  885. log.Errorf("arp Dial error %s", err)
  886. return
  887. }
  888. defer cli.Close()
  889. var (
  890. sSrcMac = nic.Mac
  891. sScrIp = nic.Ip
  892. srcIp = net.ParseIP(sScrIp)
  893. dstMac, _ = net.ParseMAC("00:00:00:00:00:00")
  894. dstIp = net.ParseIP("255.255.255.255")
  895. )
  896. srcMac, err := net.ParseMAC(sSrcMac)
  897. if err != nil {
  898. log.Errorf("Send arp parse mac error: %s", err)
  899. return
  900. }
  901. pkt, err := arp.NewPacket(arp.OperationRequest, srcMac, srcIp, dstMac, dstIp)
  902. if err != nil {
  903. log.Errorf("New arp packet error %s", err)
  904. return
  905. }
  906. if err := cli.WriteTo(pkt, ethernet.Broadcast); err != nil {
  907. log.Errorf("Send arp packet error %s ", err)
  908. return
  909. }
  910. }
  911. func (s *SKVMGuestInstance) StartPresendArp() {
  912. go func() {
  913. for i := 0; i < 5; i++ {
  914. for _, nic := range s.Desc.Nics {
  915. s.presendArpForNic(nic)
  916. }
  917. time.Sleep(1 * time.Second)
  918. }
  919. }()
  920. }
  921. func (s *SKVMGuestInstance) getPKIDirPath() string {
  922. return path.Join(s.HomeDir(), "pki")
  923. }
  924. func (s *SKVMGuestInstance) getSwtpmDirPath() string {
  925. return path.Join(s.HomeDir(), "swtpm")
  926. }
  927. func (s *SKVMGuestInstance) getSwtpmSocketPath() string {
  928. return path.Join(s.getSwtpmDirPath(), "swtpm.sock")
  929. }
  930. func (s *SKVMGuestInstance) getSwtpmLogPath() string {
  931. return path.Join(s.getSwtpmDirPath(), "swtpm.log")
  932. }
  933. func (s *SKVMGuestInstance) getSwtpmPidPath() string {
  934. return path.Join(s.getSwtpmDirPath(), "swtpm.pid")
  935. }
  936. func (s *SKVMGuestInstance) makePKIDir() error {
  937. output, err := procutils.NewCommand("mkdir", "-p", s.getPKIDirPath()).Output()
  938. if err != nil {
  939. return errors.Wrapf(err, "mkdir %s failed: %s", s.getPKIDirPath(), output)
  940. }
  941. return nil
  942. }
  943. func (s *SKVMGuestInstance) PrepareMigrateCerts() (map[string]string, error) {
  944. pkiDir := s.getPKIDirPath()
  945. if err := s.makePKIDir(); err != nil {
  946. return nil, errors.Wrap(err, "make pki dir")
  947. }
  948. tree, err := qemucerts.GetDefaultCertList().AsMap().CertTree()
  949. if err != nil {
  950. return nil, errors.Wrap(err, "construct cert tree")
  951. }
  952. if err := tree.CreateTree(pkiDir); err != nil {
  953. return nil, errors.Wrap(err, "create certs")
  954. }
  955. return qemucerts.FetchDefaultCerts(pkiDir)
  956. }
  957. func (s *SKVMGuestInstance) WriteMigrateCerts(certs map[string]string) error {
  958. pkiDir := s.getPKIDirPath()
  959. if err := s.makePKIDir(); err != nil {
  960. return errors.Wrap(err, "make pki dir")
  961. }
  962. if err := qemucerts.CreateByMap(pkiDir, certs); err != nil {
  963. return errors.Wrapf(err, "create by map %#v", certs)
  964. }
  965. return nil
  966. }
  967. func (s *SKVMGuestInstance) SetNicDown(mac string) error {
  968. var nic *desc.SGuestNetwork
  969. for i := range s.Desc.Nics {
  970. if s.Desc.Nics[i].Mac == mac {
  971. nic = s.Desc.Nics[i]
  972. break
  973. }
  974. }
  975. if nic == nil {
  976. return errors.Errorf("guest %s has no nic with mac %s", s.GetName(), mac)
  977. }
  978. scriptPath := s.getNicDownScriptPath(nic)
  979. out, err := procutils.NewRemoteCommandAsFarAsPossible("bash", scriptPath).Output()
  980. if err != nil {
  981. return errors.Wrapf(err, "failed run nic down script %s", out)
  982. }
  983. return nil
  984. }
  985. func (s *SKVMGuestInstance) SetNicUp(nic *desc.SGuestNetwork) error {
  986. scriptPath := s.getNicUpScriptPath(nic)
  987. out, err := procutils.NewRemoteCommandAsFarAsPossible("bash", scriptPath).Output()
  988. if err != nil {
  989. return errors.Wrapf(err, "failed run nic down script %s", out)
  990. }
  991. return nil
  992. }
  993. func (s *SKVMGuestInstance) startMemCleaner() error {
  994. err := procutils.NewRemoteCommandAsFarAsPossible(
  995. options.HostOptions.BinaryMemcleanPath,
  996. "--pid", strconv.Itoa(s.GetPid()),
  997. "--mem-size", strconv.FormatInt(s.Desc.Mem*1024*1024, 10),
  998. "--log-dir", s.HomeDir(),
  999. ).Run()
  1000. if err != nil {
  1001. log.Errorf("failed start memcleaner: %s", err)
  1002. return errors.Wrap(err, "start memclean")
  1003. }
  1004. return nil
  1005. }
  1006. func (s *SKVMGuestInstance) gpusHasVga() bool {
  1007. manager := s.manager.GetHost().GetIsolatedDeviceManager()
  1008. for i := 0; i < len(s.Desc.IsolatedDevices); i++ {
  1009. dev := manager.GetDeviceByAddr(s.Desc.IsolatedDevices[i].Addr)
  1010. if dev.GetDeviceType() == api.GPU_VGA_TYPE {
  1011. return true
  1012. }
  1013. }
  1014. return false
  1015. }
  1016. func (s *SKVMGuestInstance) hasGPU() bool {
  1017. manager := s.manager.GetHost().GetIsolatedDeviceManager()
  1018. for i := 0; i < len(s.Desc.IsolatedDevices); i++ {
  1019. dev := manager.GetDeviceByAddr(s.Desc.IsolatedDevices[i].Addr)
  1020. if dev.GetDeviceType() == api.GPU_VGA_TYPE || dev.GetDeviceType() == api.GPU_HPC_TYPE {
  1021. return true
  1022. }
  1023. }
  1024. return false
  1025. }
  1026. func (s *SKVMGuestInstance) initCpuDesc(cpuMax uint) error {
  1027. var err error
  1028. s.fixGuestMachineType()
  1029. if cpuMax == 0 {
  1030. cpuMax, err = s.CpuMax()
  1031. if err != nil {
  1032. return err
  1033. }
  1034. }
  1035. cpuDesc, err := s.archMan.GenerateCpuDesc(uint(s.Desc.Cpu), cpuMax, s)
  1036. if err != nil {
  1037. return err
  1038. }
  1039. s.Desc.CpuDesc = cpuDesc
  1040. // if region not allocate cpu numa pin
  1041. if len(s.Desc.CpuNumaPin) == 0 {
  1042. err = s.allocGuestNumaCpuset()
  1043. if err != nil {
  1044. return err
  1045. }
  1046. }
  1047. return nil
  1048. }
  1049. func (s *SKVMGuestInstance) initMemDesc(memSizeMB int64) error {
  1050. s.Desc.MemDesc = s.archMan.GenerateMemDesc()
  1051. s.Desc.MemDesc.SizeMB = memSizeMB
  1052. return s.initGuestMemObjects(memSizeMB)
  1053. }
  1054. func (s *SKVMGuestInstance) memObjectType() string {
  1055. if s.manager.host.IsHugepagesEnabled() {
  1056. return "memory-backend-file"
  1057. } else if s.isMemcleanEnabled() {
  1058. return "memory-backend-memfd"
  1059. } else {
  1060. return "memory-backend-ram"
  1061. }
  1062. }
  1063. func (s *SKVMGuestInstance) initGuestMemObjects(memSizeMB int64) error {
  1064. if len(s.Desc.CpuNumaPin) == 0 {
  1065. s.initDefaultMemObject(memSizeMB)
  1066. return nil
  1067. }
  1068. var numaMems int64
  1069. var numaCpus = int(s.Desc.CpuDesc.MaxCpus) / len(s.Desc.CpuNumaPin)
  1070. var leastCpus = int(s.Desc.CpuDesc.MaxCpus) % len(s.Desc.CpuNumaPin)
  1071. var cpuStart = 0
  1072. var cpuEnd = numaCpus - 1
  1073. var mems = make([]desc.SMemDesc, 0)
  1074. for i := 0; i < len(s.Desc.CpuNumaPin); i++ {
  1075. if s.Desc.CpuNumaPin[i].SizeMB <= 0 {
  1076. continue
  1077. }
  1078. numaMems += s.Desc.CpuNumaPin[i].SizeMB
  1079. memId := "mem"
  1080. nodeId := uint16(i)
  1081. if i > 0 {
  1082. memId += strconv.Itoa(i - 1)
  1083. }
  1084. if i == 0 {
  1085. cpuEnd += leastCpus
  1086. }
  1087. vcpus := fmt.Sprintf("%d-%d", cpuStart, cpuEnd)
  1088. for j := range s.Desc.CpuNumaPin[i].VcpuPin {
  1089. s.Desc.CpuNumaPin[i].VcpuPin[j].Vcpu = cpuStart + j
  1090. }
  1091. cpuStart = cpuEnd + 1
  1092. cpuEnd = cpuStart + numaCpus - 1
  1093. if s.Desc.CpuNumaPin[i].Unregular {
  1094. continue
  1095. }
  1096. memDesc := desc.NewMemDesc(s.memObjectType(), memId, &nodeId, &vcpus)
  1097. memDesc.Options = s.getMemObjectOptions(s.Desc.CpuNumaPin[i].SizeMB, s.Desc.Uuid, s.Desc.CpuNumaPin[i].NodeId)
  1098. mems = append(mems, *memDesc)
  1099. }
  1100. if len(mems) == 0 {
  1101. // numa mems not regular
  1102. s.initDefaultMemObject(memSizeMB)
  1103. return nil
  1104. }
  1105. s.Desc.MemDesc.Mem = desc.NewMemsDesc(mems[0], mems[1:])
  1106. return nil
  1107. }
  1108. func (s *SKVMGuestInstance) getMemObjectOptions(memSizeMB int64, memPathSuffix string, hostNodes *uint16) map[string]string {
  1109. var opts map[string]string
  1110. if s.manager.host.IsHugepagesEnabled() {
  1111. opts = map[string]string{
  1112. "mem-path": fmt.Sprintf("/dev/hugepages/%s", memPathSuffix),
  1113. "size": fmt.Sprintf("%dM", memSizeMB),
  1114. "share": "on", "prealloc": "on",
  1115. }
  1116. if hostNodes != nil {
  1117. opts["host-nodes"] = fmt.Sprintf("%d", *hostNodes)
  1118. opts["policy"] = "bind"
  1119. }
  1120. } else if s.isMemcleanEnabled() {
  1121. opts = map[string]string{
  1122. "size": fmt.Sprintf("%dM", memSizeMB),
  1123. "share": "on", "prealloc": "on",
  1124. }
  1125. } else {
  1126. opts = map[string]string{
  1127. "size": fmt.Sprintf("%dM", memSizeMB),
  1128. }
  1129. }
  1130. return opts
  1131. }
  1132. func (s *SKVMGuestInstance) initDefaultMemObject(memSizeMB int64) {
  1133. defaultDesc := desc.NewMemDesc(s.memObjectType(), "mem", nil, nil)
  1134. defaultDesc.Options = s.getMemObjectOptions(memSizeMB, s.Desc.Uuid, nil)
  1135. s.Desc.MemDesc.Mem = desc.NewMemsDesc(*defaultDesc, nil)
  1136. }
  1137. func (s *SKVMGuestInstance) defaultMemNodeHasObject(memDevs []monitor.Memdev) bool {
  1138. for _, dev := range memDevs {
  1139. if dev.ID != nil && *dev.ID == "mem" {
  1140. return true
  1141. }
  1142. }
  1143. return false
  1144. }
  1145. func (s *SKVMGuestInstance) initMemDescFromMemoryInfo(
  1146. memoryDevicesInfoList []monitor.MemoryDeviceInfo, memDevs []monitor.Memdev,
  1147. ) error {
  1148. memSize := s.Desc.Mem
  1149. memSlots := make([]*desc.SMemSlot, 0)
  1150. for i := 0; i < len(memoryDevicesInfoList); i++ {
  1151. if memoryDevicesInfoList[i].Type != "dimm" || memoryDevicesInfoList[i].Data.ID == nil {
  1152. return errors.Errorf("unsupported memory device type %s", memoryDevicesInfoList[i].Type)
  1153. }
  1154. memSize -= (memoryDevicesInfoList[i].Data.Size / 1024 / 1024)
  1155. memObj := desc.NewMemDesc(s.memObjectType(), path.Base(memoryDevicesInfoList[i].Data.Memdev), nil, nil)
  1156. memObj.Options = map[string]string{
  1157. "size": fmt.Sprintf("%dM", memoryDevicesInfoList[i].Data.Size/1024/1024),
  1158. }
  1159. memSlots = append(memSlots, &desc.SMemSlot{
  1160. SizeMB: memoryDevicesInfoList[i].Data.Size / 1024 / 1024,
  1161. MemObj: memObj,
  1162. MemDev: &desc.SMemDevice{
  1163. Type: "pc-dimm", Id: *memoryDevicesInfoList[i].Data.ID,
  1164. },
  1165. })
  1166. }
  1167. if memSize <= 0 {
  1168. return errors.Errorf("wrong memsize %d", s.Desc.Mem)
  1169. }
  1170. s.Desc.MemDesc = s.archMan.GenerateMemDesc()
  1171. s.Desc.MemDesc.SizeMB = memSize
  1172. if s.defaultMemNodeHasObject(memDevs) {
  1173. s.initDefaultMemObject(memSize)
  1174. }
  1175. s.Desc.MemDesc.MemSlots = memSlots
  1176. return nil
  1177. }
  1178. func (s *SKVMGuestInstance) fixGuestMachineType() {
  1179. if s.GetOsName() == OS_NAME_MACOS {
  1180. s.Desc.Machine = api.VM_MACHINE_TYPE_Q35
  1181. s.Desc.Bios = qemu.BIOS_UEFI
  1182. }
  1183. if !s.manager.host.IsX8664() {
  1184. if utils.IsInStringArray(s.Desc.Machine, []string{
  1185. "", api.VM_MACHINE_TYPE_PC, api.VM_MACHINE_TYPE_Q35,
  1186. }) {
  1187. s.Desc.Machine = api.VM_MACHINE_TYPE_VIRT
  1188. }
  1189. }
  1190. if s.manager.host.IsAarch64() || s.manager.host.IsRiscv64() {
  1191. s.Desc.Bios = qemu.BIOS_UEFI
  1192. }
  1193. }
  1194. func (s *SKVMGuestInstance) initMachineDesc() {
  1195. if s.Desc.Machine == "" {
  1196. s.Desc.Machine = s.getMachine()
  1197. }
  1198. s.Desc.MachineDesc = s.archMan.GenerateMachineDesc(s.Desc.CpuDesc.Accel)
  1199. if options.HostOptions.NoHpet {
  1200. noHpet := true
  1201. s.Desc.NoHpet = &noHpet
  1202. }
  1203. }
  1204. func (s *SKVMGuestInstance) initQgaDesc() {
  1205. s.Desc.Qga = s.archMan.GenerateQgaDesc(path.Join(s.HomeDir(), "qga.sock"))
  1206. }
  1207. func (s *SKVMGuestInstance) initPvpanicDesc() {
  1208. if !s.disablePvpanicDev() {
  1209. s.Desc.Pvpanic = s.archMan.GeneratePvpanicDesc()
  1210. }
  1211. }
  1212. func (s *SKVMGuestInstance) initIsaSerialDesc() {
  1213. if !s.disableIsaSerialDev() {
  1214. s.Desc.IsaSerial = s.archMan.GenerateIsaSerialDesc()
  1215. }
  1216. }
  1217. func (s *SKVMGuestInstance) initTpmDesc() {
  1218. if s.enableTpmDev() {
  1219. charDevId := "chrtpm"
  1220. s.Desc.Tpm = &desc.SGuestTpm{
  1221. TpmSock: desc.NewCharDev("socket", charDevId, ""),
  1222. Id: "tpm0",
  1223. }
  1224. s.Desc.Tpm.TpmSock.Options = map[string]string{
  1225. "path": s.getSwtpmSocketPath(),
  1226. }
  1227. }
  1228. }
  1229. func (s *SKVMGuestInstance) getVfioDeviceHotPlugPciControllerType() *desc.PCI_CONTROLLER_TYPE {
  1230. if s.Desc.Machine == api.VM_MACHINE_TYPE_Q35 || s.Desc.Machine == api.VM_MACHINE_TYPE_VIRT {
  1231. _, _, found := s.findUnusedSlotForController(desc.CONTROLLER_TYPE_PCIE_ROOT_PORT, 0)
  1232. if found {
  1233. var contType desc.PCI_CONTROLLER_TYPE = desc.CONTROLLER_TYPE_PCIE_ROOT_PORT
  1234. return &contType
  1235. }
  1236. return nil
  1237. } else {
  1238. return s.getHotPlugPciControllerType()
  1239. }
  1240. }
  1241. func (s *SKVMGuestInstance) getHotPlugPciControllerType() *desc.PCI_CONTROLLER_TYPE {
  1242. for i := 0; i < len(s.Desc.PCIControllers); i++ {
  1243. switch s.Desc.PCIControllers[i].CType {
  1244. case desc.CONTROLLER_TYPE_PCI_ROOT, desc.CONTROLLER_TYPE_PCI_BRIDGE:
  1245. var contType = s.Desc.PCIControllers[i].CType
  1246. return &contType
  1247. }
  1248. }
  1249. return nil
  1250. }
  1251. func (s *SKVMGuestInstance) vfioDevCount() int {
  1252. res := 0
  1253. for i := 0; i < len(s.Desc.IsolatedDevices); i++ {
  1254. if s.Desc.IsolatedDevices[i].DevType != api.USB_TYPE {
  1255. res += 1
  1256. }
  1257. }
  1258. return res
  1259. }
  1260. func (s *SKVMGuestInstance) getOvmfVarsPath() string {
  1261. ovmfVarsName := filepath.Base(options.HostOptions.OvmfVarsPath)
  1262. varsPath := path.Join(s.HomeDir(), ovmfVarsName)
  1263. if fileutils2.Exists(varsPath) {
  1264. return varsPath
  1265. }
  1266. ovmfVarsName = filepath.Base(options.HostOptions.SecbootOvmfVarsPath)
  1267. varsPath = path.Join(s.HomeDir(), ovmfVarsName)
  1268. if fileutils2.Exists(varsPath) {
  1269. return varsPath
  1270. }
  1271. ovmfVarsName = filepath.Base(options.HostOptions.Ovmf4MCodeVarsPath)
  1272. varsPath = path.Join(s.HomeDir(), ovmfVarsName)
  1273. return varsPath
  1274. }
  1275. func (s *SKVMGuestInstance) getOvmfVarsSourcePath() (string, string) {
  1276. ovmfVarsName := filepath.Base(options.HostOptions.OvmfVarsPath)
  1277. varsPath := path.Join(s.HomeDir(), ovmfVarsName)
  1278. if fileutils2.Exists(varsPath) {
  1279. return options.HostOptions.OvmfPath, options.HostOptions.OvmfVarsPath
  1280. }
  1281. ovmfVarsName = filepath.Base(options.HostOptions.SecbootOvmfVarsPath)
  1282. varsPath = path.Join(s.HomeDir(), ovmfVarsName)
  1283. if fileutils2.Exists(varsPath) {
  1284. return options.HostOptions.SecbootOvmfPath, options.HostOptions.SecbootOvmfVarsPath
  1285. }
  1286. if fileutils2.Exists(options.HostOptions.Ovmf4MCodeVarsPath) {
  1287. return options.HostOptions.Ovmf4MCodePath, options.HostOptions.Ovmf4MCodeVarsPath
  1288. } else {
  1289. return options.HostOptions.OvmfPath, options.HostOptions.OvmfVarsPath
  1290. }
  1291. }
  1292. func (s *SKVMGuestInstance) getDiskBootOrderType(driver string) uefi.OvmfDevicePathType {
  1293. switch driver {
  1294. case qemu.DISK_DRIVER_VIRTIO:
  1295. return uefi.DEVICE_TYPE_PCI
  1296. case qemu.DISK_DRIVER_SCSI, qemu.DISK_DRIVER_PVSCSI:
  1297. return uefi.DEVICE_TYPE_SCSI
  1298. case qemu.DISK_DRIVER_IDE:
  1299. if !s.manager.host.IsX8664() {
  1300. return uefi.DEVICE_TYPE_SCSI
  1301. }
  1302. return uefi.DEVICE_TYPE_IDE
  1303. case qemu.DISK_DRIVER_SATA:
  1304. if !s.manager.host.IsX8664() {
  1305. return uefi.DEVICE_TYPE_SCSI
  1306. }
  1307. return uefi.DEVICE_TYPE_SATA
  1308. }
  1309. return uefi.DEVICE_TYPE_UNKNOWN
  1310. }
  1311. func (s *SKVMGuestInstance) getCdromBootOrder() uefi.OvmfDevicePathType {
  1312. if !s.manager.host.IsX8664() {
  1313. return uefi.DEVICE_TYPE_SCSI_CDROM
  1314. }
  1315. return uefi.DEVICE_TYPE_CDROM
  1316. }
  1317. func (s *SKVMGuestInstance) setUefiBootOrder(ctx context.Context) error {
  1318. params := &deployapi.OvmfBootOrderParams{
  1319. OvmfVarsPath: s.getOvmfVarsPath(),
  1320. }
  1321. devs := make([]*deployapi.BootDevices, 0)
  1322. for i := range s.Desc.Cdroms {
  1323. if s.Desc.Cdroms[i].BootIndex == nil || *s.Desc.Disks[i].BootIndex < 0 {
  1324. continue
  1325. }
  1326. dev := &deployapi.BootDevices{
  1327. BootOrder: int32(*s.Desc.Cdroms[i].BootIndex),
  1328. AttachOrder: int32(s.Desc.Cdroms[i].Ordinal),
  1329. DevType: int32(s.getCdromBootOrder()),
  1330. }
  1331. devs = append(devs, dev)
  1332. }
  1333. for i := range s.Desc.Disks {
  1334. if s.Desc.Disks[i].BootIndex == nil || *s.Desc.Disks[i].BootIndex < 0 {
  1335. continue
  1336. }
  1337. dev := &deployapi.BootDevices{
  1338. BootOrder: int32(*s.Desc.Disks[i].BootIndex),
  1339. AttachOrder: int32(s.Desc.Disks[i].Index),
  1340. DevType: int32(s.getDiskBootOrderType(s.Desc.Disks[i].Driver)),
  1341. }
  1342. devs = append(devs, dev)
  1343. }
  1344. params.Devs = devs
  1345. _, err := deployclient.GetDeployClient().SetOvmfBootOrder(ctx, params)
  1346. if err != nil {
  1347. return errors.Wrap(err, "SetOvmfBootOrder")
  1348. }
  1349. return nil
  1350. }
  1351. // attachKickstartISO attaches the kickstart ISO as an additional CDROM device
  1352. // if the kickstart is not provided by URL
  1353. func (s *SKVMGuestInstance) attachKickstartISO(isoPath string) error {
  1354. cdromId := fmt.Sprintf("kickstart_iso_%s", s.Id)
  1355. log.Debugf("Attaching kickstart ISO %s as CDROM device for guest %s", isoPath, s.GetName())
  1356. kickstartCdrom := &desc.SGuestCdrom{
  1357. Id: cdromId,
  1358. Path: isoPath,
  1359. Ordinal: int64(len(s.Desc.Cdroms)),
  1360. Scsi: desc.NewScsiDevice("scsi.0", "scsi-cd", fmt.Sprintf("scsi-cd-%s", cdromId)),
  1361. DriveOptions: map[string]string{
  1362. "readonly": "on",
  1363. "media": "cdrom",
  1364. "if": "none",
  1365. },
  1366. }
  1367. s.Desc.Cdroms = append(s.Desc.Cdroms, kickstartCdrom)
  1368. log.Debugf("Successfully attached kickstart ISO %s as SCSI CDROM device %s (ordinal=%d) for guest %s",
  1369. isoPath, cdromId, kickstartCdrom.Ordinal, s.GetName())
  1370. return nil
  1371. }