本文首发于泊浮目的专栏:https://segmentfault.com/blog/camile

背景

在上篇文章中(ZStack源码剖析之二次开发——可扩展框架
),我们简单的了解了一下ZStack核心引擎的二次开发技巧。在这篇文章中,我们将一起来了解[ZStack-Utility](https://github.com/zstackio/zstack-utility)(即ZStack的Agent端)的二开姿势。

例子

我们以ZStack管理节点调用startVm这个api为例子,一起来看一下在agent上的执行逻辑。

  1. def start(self):
  2. http_server = kvmagent.get_http_server()
  3. http_server.register_async_uri(self.KVM_START_VM_PATH, self.start_vm)

首先,得注册一个http path用来接受reqeust。

  1. @kvmagent.replyerror
  2. def start_vm(self, req):
  3. cmd = jsonobject.loads(req[http.REQUEST_BODY])
  4. rsp = StartVmResponse()
  5. try:
  6. self._record_operation(cmd.vmInstanceUuid, self.VM_OP_START)
  7. self._start_vm(cmd)
  8. logger.debug('successfully started vm[uuid:%s, name:%s]' % (cmd.vmInstanceUuid, cmd.vmName))
  9. except kvmagent.KvmError as e:
  10. e_str = linux.get_exception_stacktrace()
  11. logger.warn(e_str)
  12. if "burst" in e_str and "Illegal" in e_str and "rate" in e_str:
  13. rsp.error = "QoS exceed max limit, please check and reset it in zstack"
  14. elif "cannot set up guest memory" in e_str:
  15. logger.warn('unable to start vm[uuid:%s], %s' % (cmd.vmInstanceUuid, e_str))
  16. rsp.error = "No enough physical memory for guest"
  17. else:
  18. rsp.error = e_str
  19. err = self.handle_vfio_irq_conflict(cmd.vmInstanceUuid)
  20. if err != "":
  21. rsp.error = "%s, details: %s" % (err, rsp.error)
  22. rsp.success = False
  23. return jsonobject.dumps(rsp)

直接进入主干逻辑,self._start_vm(cmd)

  1. @lock.lock('libvirt-startvm')
  2. def _start_vm(self, cmd):
  3. try:
  4. vm = get_vm_by_uuid_no_retry(cmd.vmInstanceUuid, False)
  5. if vm:
  6. if vm.state == Vm.VM_STATE_RUNNING:
  7. raise kvmagent.KvmError(
  8. 'vm[uuid:%s, name:%s] is already running' % (cmd.vmInstanceUuid, vm.get_name()))
  9. else:
  10. vm.destroy()
  11. vm = Vm.from_StartVmCmd(cmd)
  12. vm.start(cmd.timeout)
  13. except libvirt.libvirtError as e:
  14. logger.warn(linux.get_exception_stacktrace())
  15. if "Device or resource busy" in str(e.message):
  16. raise kvmagent.KvmError(
  17. 'unable to start vm[uuid:%s, name:%s], libvirt error: %s' % (
  18. cmd.vmInstanceUuid, cmd.vmName, str(e)))
  19. try:
  20. vm = get_vm_by_uuid(cmd.vmInstanceUuid)
  21. if vm and vm.state != Vm.VM_STATE_RUNNING:
  22. raise kvmagent.KvmError(
  23. 'vm[uuid:%s, name:%s, state:%s] is not in running state, libvirt error: %s' % (
  24. cmd.vmInstanceUuid, cmd.vmName, vm.state, str(e)))
  25. except kvmagent.KvmError:
  26. raise kvmagent.KvmError(
  27. 'unable to start vm[uuid:%s, name:%s], libvirt error: %s' % (cmd.vmInstanceUuid, cmd.vmName, str(e)))

关键逻辑:

  1. vm = Vm.from_StartVmCmd(cmd)
  2. vm.start(cmd.timeout)

先看from_StartVmCmd

  1. @staticmethod
  2. def from_StartVmCmd(cmd):
  3. use_virtio = cmd.useVirtio
  4. use_numa = cmd.useNuma
  5. elements = {}
  6. def make_root():
  7. root = etree.Element('domain')
  8. root.set('type', 'kvm')
  9. # self._root.set('type', 'qemu')
  10. root.set('xmlns:qemu', 'http://libvirt.org/schemas/domain/qemu/1.0')
  11. elements['root'] = root
  12. def make_cpu():
  13. if use_numa:
  14. root = elements['root']
  15. e(root, 'vcpu', '128', {'placement': 'static', 'current': str(cmd.cpuNum)})
  16. # e(root,'vcpu',str(cmd.cpuNum),{'placement':'static'})
  17. tune = e(root, 'cputune')
  18. e(tune, 'shares', str(cmd.cpuSpeed * cmd.cpuNum))
  19. # enable nested virtualization
  20. if cmd.nestedVirtualization == 'host-model':
  21. cpu = e(root, 'cpu', attrib={'mode': 'host-model'})
  22. e(cpu, 'model', attrib={'fallback': 'allow'})
  23. elif cmd.nestedVirtualization == 'host-passthrough':
  24. cpu = e(root, 'cpu', attrib={'mode': 'host-passthrough'})
  25. e(cpu, 'model', attrib={'fallback': 'allow'})
  26. elif IS_AARCH64:
  27. cpu = e(root, 'cpu', attrib={'mode': 'host-passthrough'})
  28. e(cpu, 'model', attrib={'fallback': 'allow'})
  29. else:
  30. cpu = e(root, 'cpu')
  31. # e(cpu, 'topology', attrib={'sockets': str(cmd.socketNum), 'cores': str(cmd.cpuOnSocket), 'threads': '1'})
  32. mem = cmd.memory / 1024
  33. e(cpu, 'topology', attrib={'sockets': str(32), 'cores': str(4), 'threads': '1'})
  34. numa = e(cpu, 'numa')
  35. e(numa, 'cell', attrib={'id': '0', 'cpus': '0-127', 'memory': str(mem), 'unit': 'KiB'})
  36. else:
  37. root = elements['root']
  38. # e(root, 'vcpu', '128', {'placement': 'static', 'current': str(cmd.cpuNum)})
  39. e(root, 'vcpu', str(cmd.cpuNum), {'placement': 'static'})
  40. tune = e(root, 'cputune')
  41. e(tune, 'shares', str(cmd.cpuSpeed * cmd.cpuNum))
  42. # enable nested virtualization
  43. if cmd.nestedVirtualization == 'host-model':
  44. cpu = e(root, 'cpu', attrib={'mode': 'host-model'})
  45. e(cpu, 'model', attrib={'fallback': 'allow'})
  46. elif cmd.nestedVirtualization == 'host-passthrough':
  47. cpu = e(root, 'cpu', attrib={'mode': 'host-passthrough'})
  48. e(cpu, 'model', attrib={'fallback': 'allow'})
  49. elif IS_AARCH64:
  50. cpu = e(root, 'cpu', attrib={'mode': 'host-passthrough'})
  51. e(cpu, 'model', attrib={'fallback': 'allow'})
  52. else:
  53. cpu = e(root, 'cpu')
  54. e(cpu, 'topology', attrib={'sockets': str(cmd.socketNum), 'cores': str(cmd.cpuOnSocket), 'threads': '1'})
  55. def make_memory():
  56. root = elements['root']
  57. mem = cmd.memory / 1024
  58. if use_numa:
  59. e(root, 'maxMemory', str(68719476736), {'slots': str(16), 'unit': 'KiB'})
  60. # e(root,'memory',str(mem),{'unit':'k'})
  61. e(root, 'currentMemory', str(mem), {'unit': 'k'})
  62. else:
  63. e(root, 'memory', str(mem), {'unit': 'k'})
  64. e(root, 'currentMemory', str(mem), {'unit': 'k'})
  65. def make_os():
  66. root = elements['root']
  67. os = e(root, 'os')
  68. if IS_AARCH64:
  69. e(os, 'type', 'hvm', attrib={'arch': 'aarch64'})
  70. e(os, 'loader', '/usr/share/edk2.git/aarch64/QEMU_EFI-pflash.raw', attrib={'readonly': 'yes', 'type': 'pflash'})
  71. else:
  72. e(os, 'type', 'hvm', attrib={'machine': 'pc'})
  73. # if not booting from cdrom, don't add any boot element in os section
  74. if cmd.bootDev[0] == "cdrom":
  75. for boot_dev in cmd.bootDev:
  76. e(os, 'boot', None, {'dev': boot_dev})
  77. if cmd.useBootMenu:
  78. e(os, 'bootmenu', attrib={'enable': 'yes'})
  79. def make_features():
  80. root = elements['root']
  81. features = e(root, 'features')
  82. for f in ['acpi', 'apic', 'pae']:
  83. e(features, f)
  84. if cmd.kvmHiddenState == True:
  85. kvm = e(features, "kvm")
  86. e(kvm, 'hidden', None, {'state': 'on'})
  87. def make_devices():
  88. root = elements['root']
  89. devices = e(root, 'devices')
  90. if cmd.addons and cmd.addons['qemuPath']:
  91. e(devices, 'emulator', cmd.addons['qemuPath'])
  92. else:
  93. e(devices, 'emulator', kvmagent.get_qemu_path())
  94. tablet = e(devices, 'input', None, {'type': 'tablet', 'bus': 'usb'})
  95. e(tablet, 'address', None, {'type':'usb', 'bus':'0', 'port':'1'})
  96. if IS_AARCH64:
  97. keyboard = e(devices, 'input', None, {'type': 'keyboard', 'bus': 'usb'})
  98. elements['devices'] = devices
  99. def make_cdrom():
  100. devices = elements['devices']
  101. MAX_CDROM_NUM = len(Vm.ISO_DEVICE_LETTERS)
  102. EMPTY_CDROM_CONFIGS = None
  103. if IS_AARCH64:
  104. # AArch64 Does not support the attachment of multiple iso
  105. EMPTY_CDROM_CONFIGS = [
  106. EmptyCdromConfig(None, None, None)
  107. ]
  108. else:
  109. # bus 0 unit 0 already use by root volume
  110. EMPTY_CDROM_CONFIGS = [
  111. EmptyCdromConfig('hd%s' % Vm.ISO_DEVICE_LETTERS[0], '0', '1'),
  112. EmptyCdromConfig('hd%s' % Vm.ISO_DEVICE_LETTERS[1], '1', '0'),
  113. EmptyCdromConfig('hd%s' % Vm.ISO_DEVICE_LETTERS[2], '1', '1')
  114. ]
  115. if len(EMPTY_CDROM_CONFIGS) != MAX_CDROM_NUM:
  116. logger.error('ISO_DEVICE_LETTERS or EMPTY_CDROM_CONFIGS config error')
  117. def makeEmptyCdrom(targetDev, bus, unit):
  118. cdrom = e(devices, 'disk', None, {'type': 'file', 'device': 'cdrom'})
  119. e(cdrom, 'driver', None, {'name': 'qemu', 'type': 'raw'})
  120. if IS_AARCH64:
  121. e(cdrom, 'target', None, {'dev': 'sdc', 'bus': 'scsi'})
  122. else:
  123. e(cdrom, 'target', None, {'dev': targetDev, 'bus': 'ide'})
  124. e(cdrom, 'address', None,{'type' : 'drive', 'bus' : bus, 'unit' : unit})
  125. e(cdrom, 'readonly', None)
  126. return cdrom
  127. if not cmd.bootIso:
  128. for config in EMPTY_CDROM_CONFIGS:
  129. makeEmptyCdrom(config.targetDev, config.bus, config.unit)
  130. return
  131. notEmptyCdrom = set([])
  132. for iso in cmd.bootIso:
  133. notEmptyCdrom.add(iso.deviceId)
  134. cdromConfig = EMPTY_CDROM_CONFIGS[iso.deviceId]
  135. if iso.path.startswith('ceph'):
  136. ic = IsoCeph()
  137. ic.iso = iso
  138. devices.append(ic.to_xmlobject(cdromConfig.targetDev, cdromConfig.bus , cdromConfig.unit))
  139. elif iso.path.startswith('fusionstor'):
  140. ic = IsoFusionstor()
  141. ic.iso = iso
  142. devices.append(ic.to_xmlobject(cdromConfig.targetDev, cdromConfig.bus , cdromConfig.unit))
  143. else:
  144. cdrom = makeEmptyCdrom(cdromConfig.targetDev, cdromConfig.bus , cdromConfig.unit)
  145. e(cdrom, 'source', None, {'file': iso.path})
  146. emptyCdrom = set(range(MAX_CDROM_NUM)).difference(notEmptyCdrom)
  147. for i in emptyCdrom:
  148. cdromConfig = EMPTY_CDROM_CONFIGS[i]
  149. makeEmptyCdrom(cdromConfig.targetDev, cdromConfig.bus, cdromConfig.unit)
  150. def make_volumes():
  151. devices = elements['devices']
  152. volumes = [cmd.rootVolume]
  153. volumes.extend(cmd.dataVolumes)
  154. def filebased_volume(_dev_letter, _v):
  155. disk = etree.Element('disk', {'type': 'file', 'device': 'disk', 'snapshot': 'external'})
  156. e(disk, 'driver', None, {'name': 'qemu', 'type': linux.get_img_fmt(_v.installPath), 'cache': _v.cacheMode})
  157. e(disk, 'source', None, {'file': _v.installPath})
  158. if _v.shareable:
  159. e(disk, 'shareable')
  160. if _v.useVirtioSCSI:
  161. e(disk, 'target', None, {'dev': 'sd%s' % _dev_letter, 'bus': 'scsi'})
  162. e(disk, 'wwn', _v.wwn)
  163. e(disk, 'address', None, {'type': 'drive', 'controller': '0', 'unit': str(_v.deviceId)})
  164. return disk
  165. if _v.useVirtio:
  166. e(disk, 'target', None, {'dev': 'vd%s' % _dev_letter, 'bus': 'virtio'})
  167. elif IS_AARCH64:
  168. e(disk, 'target', None, {'dev': 'sd%s' % _dev_letter, 'bus': 'scsi'})
  169. else:
  170. e(disk, 'target', None, {'dev': 'sd%s' % _dev_letter, 'bus': 'ide'})
  171. return disk
  172. def iscsibased_volume(_dev_letter, _v):
  173. def blk_iscsi():
  174. bi = BlkIscsi()
  175. portal, bi.target, bi.lun = _v.installPath.lstrip('iscsi://').split('/')
  176. bi.server_hostname, bi.server_port = portal.split(':')
  177. bi.device_letter = _dev_letter
  178. bi.volume_uuid = _v.volumeUuid
  179. bi.chap_username = _v.chapUsername
  180. bi.chap_password = _v.chapPassword
  181. return bi.to_xmlobject()
  182. def virtio_iscsi():
  183. vi = VirtioIscsi()
  184. portal, vi.target, vi.lun = _v.installPath.lstrip('iscsi://').split('/')
  185. vi.server_hostname, vi.server_port = portal.split(':')
  186. vi.device_letter = _dev_letter
  187. vi.volume_uuid = _v.volumeUuid
  188. vi.chap_username = _v.chapUsername
  189. vi.chap_password = _v.chapPassword
  190. return vi.to_xmlobject()
  191. if _v.useVirtio:
  192. return virtio_iscsi()
  193. else:
  194. return blk_iscsi()
  195. def ceph_volume(_dev_letter, _v):
  196. def ceph_virtio():
  197. vc = VirtioCeph()
  198. vc.volume = _v
  199. vc.dev_letter = _dev_letter
  200. return vc.to_xmlobject()
  201. def ceph_blk():
  202. if not IS_AARCH64:
  203. ic = IdeCeph()
  204. else:
  205. ic = ScsiCeph()
  206. ic.volume = _v
  207. ic.dev_letter = _dev_letter
  208. return ic.to_xmlobject()
  209. def ceph_virtio_scsi():
  210. vsc = VirtioSCSICeph()
  211. vsc.volume = _v
  212. vsc.dev_letter = _dev_letter
  213. return vsc.to_xmlobject()
  214. if _v.useVirtioSCSI:
  215. disk = ceph_virtio_scsi()
  216. if _v.shareable:
  217. e(disk, 'shareable')
  218. return disk
  219. if _v.useVirtio:
  220. return ceph_virtio()
  221. else:
  222. return ceph_blk()
  223. def fusionstor_volume(_dev_letter, _v):
  224. def fusionstor_virtio():
  225. vc = VirtioFusionstor()
  226. vc.volume = _v
  227. vc.dev_letter = _dev_letter
  228. return vc.to_xmlobject()
  229. def fusionstor_blk():
  230. ic = IdeFusionstor()
  231. ic.volume = _v
  232. ic.dev_letter = _dev_letter
  233. return ic.to_xmlobject()
  234. def fusionstor_virtio_scsi():
  235. vsc = VirtioSCSIFusionstor()
  236. vsc.volume = _v
  237. vsc.dev_letter = _dev_letter
  238. return vsc.to_xmlobject()
  239. if _v.useVirtioSCSI:
  240. disk = fusionstor_virtio_scsi()
  241. if _v.shareable:
  242. e(disk, 'shareable')
  243. return disk
  244. if _v.useVirtio:
  245. return fusionstor_virtio()
  246. else:
  247. return fusionstor_blk()
  248. def volume_qos(volume_xml_obj):
  249. if not cmd.addons:
  250. return
  251. vol_qos = cmd.addons['VolumeQos']
  252. if not vol_qos:
  253. return
  254. qos = vol_qos[v.volumeUuid]
  255. if not qos:
  256. return
  257. if not qos.totalBandwidth and not qos.totalIops:
  258. return
  259. iotune = e(volume_xml_obj, 'iotune')
  260. if qos.totalBandwidth:
  261. e(iotune, 'total_bytes_sec', str(qos.totalBandwidth))
  262. if qos.totalIops:
  263. # e(iotune, 'total_iops_sec', str(qos.totalIops))
  264. e(iotune, 'read_iops_sec', str(qos.totalIops))
  265. e(iotune, 'write_iops_sec', str(qos.totalIops))
  266. # e(iotune, 'read_iops_sec_max', str(qos.totalIops))
  267. # e(iotune, 'write_iops_sec_max', str(qos.totalIops))
  268. # e(iotune, 'total_iops_sec_max', str(qos.totalIops))
  269. volumes.sort(key=lambda d: d.deviceId)
  270. scsi_device_ids = [v.deviceId for v in volumes if v.useVirtioSCSI]
  271. for v in volumes:
  272. if v.deviceId >= len(Vm.DEVICE_LETTERS):
  273. err = "exceeds max disk limit, it's %s but only 26 allowed" % v.deviceId
  274. logger.warn(err)
  275. raise kvmagent.KvmError(err)
  276. dev_letter = Vm.DEVICE_LETTERS[v.deviceId]
  277. if v.useVirtioSCSI:
  278. dev_letter = Vm.DEVICE_LETTERS[scsi_device_ids.pop()]
  279. if v.deviceType == 'file':
  280. vol = filebased_volume(dev_letter, v)
  281. elif v.deviceType == 'iscsi':
  282. vol = iscsibased_volume(dev_letter, v)
  283. elif v.deviceType == 'ceph':
  284. vol = ceph_volume(dev_letter, v)
  285. elif v.deviceType == 'fusionstor':
  286. vol = fusionstor_volume(dev_letter, v)
  287. else:
  288. raise Exception('unknown volume deviceType: %s' % v.deviceType)
  289. assert vol is not None, 'vol cannot be None'
  290. # set boot order for root volume when boot from hd
  291. if v.deviceId == 0 and cmd.bootDev[0] == 'hd' and cmd.useBootMenu:
  292. e(vol, 'boot', None, {'order': '1'})
  293. volume_qos(vol)
  294. devices.append(vol)
  295. def make_nics():
  296. if not cmd.nics:
  297. return
  298. def nic_qos(nic_xml_object):
  299. if not cmd.addons:
  300. return
  301. nqos = cmd.addons['NicQos']
  302. if not nqos:
  303. return
  304. qos = nqos[nic.uuid]
  305. if not qos:
  306. return
  307. if not qos.outboundBandwidth and not qos.inboundBandwidth:
  308. return
  309. bandwidth = e(nic_xml_object, 'bandwidth')
  310. if qos.outboundBandwidth:
  311. e(bandwidth, 'outbound', None, {'average': str(qos.outboundBandwidth / 1024 / 8)})
  312. if qos.inboundBandwidth:
  313. e(bandwidth, 'inbound', None, {'average': str(qos.inboundBandwidth / 1024 / 8)})
  314. devices = elements['devices']
  315. for nic in cmd.nics:
  316. interface = e(devices, 'interface', None, {'type': 'bridge'})
  317. e(interface, 'mac', None, {'address': nic.mac})
  318. if nic.ip is not None and nic.ip != "":
  319. filterref = e(interface, 'filterref', None, {'filter':'clean-traffic'})
  320. e(filterref, 'parameter', None, {'name':'IP', 'value': nic.ip})
  321. e(interface, 'alias', None, {'name': 'net%s' % nic.nicInternalName.split('.')[1]})
  322. e(interface, 'source', None, {'bridge': nic.bridgeName})
  323. if use_virtio:
  324. e(interface, 'model', None, {'type': 'virtio'})
  325. else:
  326. e(interface, 'model', None, {'type': 'e1000'})
  327. e(interface, 'target', None, {'dev': nic.nicInternalName})
  328. nic_qos(interface)
  329. def make_meta():
  330. root = elements['root']
  331. e(root, 'name', cmd.vmInstanceUuid)
  332. e(root, 'uuid', uuidhelper.to_full_uuid(cmd.vmInstanceUuid))
  333. e(root, 'description', cmd.vmName)
  334. e(root, 'on_poweroff', 'destroy')
  335. e(root, 'on_crash', 'restart')
  336. e(root, 'on_reboot', 'restart')
  337. meta = e(root, 'metadata')
  338. zs = e(meta, 'zstack', usenamesapce=True)
  339. e(zs, 'internalId', str(cmd.vmInternalId))
  340. e(zs, 'hostManagementIp', str(cmd.hostManagementIp))
  341. clock = e(root, 'clock', None, {'offset': cmd.clock})
  342. if cmd.clock == 'localtime':
  343. e(clock, 'timer', None, {'name': 'rtc', 'tickpolicy': 'catchup'})
  344. e(clock, 'timer', None, {'name': 'pit', 'tickpolicy': 'delay'})
  345. e(clock, 'timer', None, {'name': 'hpet', 'present': 'no'})
  346. e(clock, 'timer', None, {'name': 'hypervclock', 'present': 'yes'})
  347. def make_vnc():
  348. devices = elements['devices']
  349. if cmd.consolePassword == None:
  350. vnc = e(devices, 'graphics', None, {'type': 'vnc', 'port': '5900', 'autoport': 'yes'})
  351. else:
  352. vnc = e(devices, 'graphics', None,
  353. {'type': 'vnc', 'port': '5900', 'autoport': 'yes', 'passwd': str(cmd.consolePassword)})
  354. e(vnc, "listen", None, {'type': 'address', 'address': '0.0.0.0'})
  355. def make_spice():
  356. devices = elements['devices']
  357. spice = e(devices, 'graphics', None, {'type': 'spice', 'port': '5900', 'autoport': 'yes'})
  358. e(spice, "listen", None, {'type': 'address', 'address': '0.0.0.0'})
  359. e(spice, "image", None, {'compression': 'auto_glz'})
  360. e(spice, "jpeg", None, {'compression': 'always'})
  361. e(spice, "zlib", None, {'compression': 'never'})
  362. e(spice, "playback", None, {'compression': 'off'})
  363. e(spice, "streaming", None, {'mode': cmd.spiceStreamingMode})
  364. e(spice, "mouse", None, {'mode': 'client'})
  365. e(spice, "filetransfer", None, {'enable': 'no'})
  366. e(spice, "clipboard", None, {'copypaste': 'no'})
  367. def make_usb_redirect():
  368. if cmd.usbRedirect == "true":
  369. devices = elements['devices']
  370. e(devices, 'controller', None, {'type': 'usb', 'model': 'ich9-ehci1'})
  371. e(devices, 'controller', None, {'type': 'usb', 'model': 'ich9-uhci1', 'multifunction': 'on'})
  372. e(devices, 'controller', None, {'type': 'usb', 'model': 'ich9-uhci2'})
  373. e(devices, 'controller', None, {'type': 'usb', 'model': 'ich9-uhci3'})
  374. chan = e(devices, 'channel', None, {'type': 'spicevmc'})
  375. e(chan, 'target', None, {'type': 'virtio', 'name': 'com.redhat.spice.0'})
  376. e(chan, 'address', None, {'type': 'virtio-serial'})
  377. redirdev2 = e(devices, 'redirdev', None, {'type': 'spicevmc', 'bus': 'usb'})
  378. e(redirdev2, 'address', None, {'type': 'usb', 'bus': '0', 'port': '2'})
  379. redirdev3 = e(devices, 'redirdev', None, {'type': 'spicevmc', 'bus': 'usb'})
  380. e(redirdev3, 'address', None, {'type': 'usb', 'bus': '0', 'port': '3'})
  381. redirdev4 = e(devices, 'redirdev', None, {'type': 'spicevmc', 'bus': 'usb'})
  382. e(redirdev4, 'address', None, {'type': 'usb', 'bus': '0', 'port': '4'})
  383. redirdev5 = e(devices, 'redirdev', None, {'type': 'spicevmc', 'bus': 'usb'})
  384. e(redirdev5, 'address', None, {'type': 'usb', 'bus': '0', 'port': '6'})
  385. else:
  386. # make sure there are three default usb controllers, for usb 1.1/2.0/3.0
  387. devices = elements['devices']
  388. e(devices, 'controller', None, {'type': 'usb', 'index': '0'})
  389. if not IS_AARCH64:
  390. e(devices, 'controller', None, {'type': 'usb', 'index': '1', 'model': 'ehci'})
  391. e(devices, 'controller', None, {'type': 'usb', 'index': '2', 'model': 'nec-xhci'})
  392. def make_video():
  393. devices = elements['devices']
  394. if IS_AARCH64:
  395. video = e(devices, 'video')
  396. e(video, 'model', None, {'type': 'virtio'})
  397. elif cmd.videoType != "qxl":
  398. video = e(devices, 'video')
  399. e(video, 'model', None, {'type': str(cmd.videoType)})
  400. else:
  401. for monitor in range(cmd.VDIMonitorNumber):
  402. video = e(devices, 'video')
  403. e(video, 'model', None, {'type': str(cmd.videoType)})
  404. def make_audio_microphone():
  405. if cmd.consoleMode == 'spice':
  406. devices = elements['devices']
  407. e(devices, 'sound',None,{'model':'ich6'})
  408. else:
  409. return
  410. def make_graphic_console():
  411. if cmd.consoleMode == 'spice':
  412. make_spice()
  413. else:
  414. make_vnc()
  415. def make_addons():
  416. if not cmd.addons:
  417. return
  418. devices = elements['devices']
  419. channel = cmd.addons['channel']
  420. if channel:
  421. basedir = os.path.dirname(channel.socketPath)
  422. linux.mkdir(basedir, 0777)
  423. chan = e(devices, 'channel', None, {'type': 'unix'})
  424. e(chan, 'source', None, {'mode': 'bind', 'path': channel.socketPath})
  425. e(chan, 'target', None, {'type': 'virtio', 'name': channel.targetName})
  426. cephSecretKey = cmd.addons['ceph_secret_key']
  427. cephSecretUuid = cmd.addons['ceph_secret_uuid']
  428. if cephSecretKey and cephSecretUuid:
  429. VmPlugin._create_ceph_secret_key(cephSecretKey, cephSecretUuid)
  430. pciDevices = cmd.addons['pciDevice']
  431. if pciDevices:
  432. make_pci_device(pciDevices)
  433. usbDevices = cmd.addons['usbDevice']
  434. if usbDevices:
  435. make_usb_device(usbDevices)
  436. def make_pci_device(addresses):
  437. devices = elements['devices']
  438. for addr in addresses:
  439. if match_pci_device(addr):
  440. hostdev = e(devices, "hostdev", None, {'mode': 'subsystem', 'type': 'pci', 'managed': 'yes'})
  441. e(hostdev, "driver", None, {'name': 'vfio'})
  442. source = e(hostdev, "source")
  443. e(source, "address", None, {
  444. "domain": hex(0) if len(addr.split(":")) == 2 else hex(int(addr.split(":")[0], 16)),
  445. "bus": hex(int(addr.split(":")[-2], 16)),
  446. "slot": hex(int(addr.split(":")[-1].split(".")[0], 16)),
  447. "function": hex(int(addr.split(":")[-1].split(".")[1], 16))
  448. })
  449. else:
  450. raise kvmagent.KvmError(
  451. 'can not find pci device for address %s' % addr)
  452. def make_usb_device(usbDevices):
  453. next_uhci_port = 2
  454. next_ehci_port = 1
  455. next_xhci_port = 1
  456. devices = elements['devices']
  457. for usb in usbDevices:
  458. if match_usb_device(usb):
  459. hostdev = e(devices, "hostdev", None, {'mode': 'subsystem', 'type': 'usb', 'managed': 'yes'})
  460. source = e(hostdev, "source")
  461. e(source, "address", None, {
  462. "bus": str(int(usb.split(":")[0])),
  463. "device": str(int(usb.split(":")[1]))
  464. })
  465. e(source, "vendor", None, {
  466. "id": hex(int(usb.split(":")[2], 16))
  467. })
  468. e(source, "product", None, {
  469. "id": hex(int(usb.split(":")[3], 16))
  470. })
  471. # get controller index from usbVersion
  472. # eg. 1.1 -> 0
  473. # eg. 2.0.0 -> 1
  474. # eg. 3 -> 2
  475. bus = int(usb.split(":")[4][0]) - 1
  476. if bus == 0:
  477. address = e(hostdev, "address", None, {'type': 'usb', 'bus': str(bus), 'port': str(next_uhci_port)})
  478. next_uhci_port += 1
  479. elif bus == 1:
  480. address = e(hostdev, "address", None, {'type': 'usb', 'bus': str(bus), 'port': str(next_ehci_port)})
  481. next_ehci_port += 1
  482. elif bus == 2:
  483. address = e(hostdev, "address", None, {'type': 'usb', 'bus': str(bus), 'port': str(next_xhci_port)})
  484. next_xhci_port += 1
  485. else:
  486. raise kvmagent.KvmError('unknown usb controller %s', bus)
  487. else:
  488. raise kvmagent.KvmError('cannot find usb device %s', usb)
  489. # TODO(WeiW) Validate here
  490. def match_pci_device(addr):
  491. return True
  492. def match_usb_device(addr):
  493. if len(addr.split(':')) == 5:
  494. return True
  495. else:
  496. return False
  497. def make_balloon_memory():
  498. devices = elements['devices']
  499. b = e(devices, 'memballoon', None, {'model': 'virtio'})
  500. e(b, 'stats', None, {'period': '10'})
  501. def make_console():
  502. devices = elements['devices']
  503. serial = e(devices, 'serial', None, {'type': 'pty'})
  504. e(serial, 'target', None, {'port': '0'})
  505. console = e(devices, 'console', None, {'type': 'pty'})
  506. e(console, 'target', None, {'type': 'serial', 'port': '0'})
  507. def make_sec_label():
  508. root = elements['root']
  509. e(root, 'seclabel', None, {'type': 'none'})
  510. def make_controllers():
  511. devices = elements['devices']
  512. e(devices, 'controller', None, {'type': 'scsi', 'model': 'virtio-scsi'})
  513. make_root()
  514. make_meta()
  515. make_cpu()
  516. make_memory()
  517. make_os()
  518. make_features()
  519. make_devices()
  520. make_video()
  521. make_audio_microphone()
  522. make_nics()
  523. make_volumes()
  524. make_cdrom()
  525. make_graphic_console()
  526. make_usb_redirect()
  527. make_addons()
  528. make_balloon_memory()
  529. make_console()
  530. make_sec_label()
  531. make_controllers()
  532. root = elements['root']
  533. xml = etree.tostring(root)
  534. vm = Vm()
  535. vm.uuid = cmd.vmInstanceUuid
  536. vm.domain_xml = xml
  537. vm.domain_xmlobject = xmlobject.loads(xml)
  538. return vm

显然,上述逻辑是在组装一份xml,便于之后的libvirt使用。

然后是

  1. vm.start(cmd.timeout)

可以看到,这里是直接调用了libvirt的sdk。

这仅仅是一个调用流程。而在很多地方,来自MN的请求会直接调用linux的shell命令,详情见linux.py。(获取云盘大小、主存储容量等)。

问题

在基于扩展ZStack的Agent时,如果是一个全新的功能模块,可能并不会造成和原有代码的深度耦合。但如果在原有功能上的增强, 对原有代码进行修改可能会导致我们的业务逻辑和Utility的上游代码耦合。而在没有足够人力来维护、开发ZStack时,我们会将目标定为能够及时跟上发布版本。 因此,我们要尽量减少冲突。

举个例子:我们要对启动vm的逻辑进行增强,添加一个自己的配置写入xml。这段代码如果写进了vm_plugin.py,那么就是一个耦合。耦合多了以后,跟上发布版本就会很困难。

解决方案

这是一个参考方案:

如果是引入一个全新的功能模块,建议重写一个项目。无论是代码规范还是自动化测试,都可以有一个很好的实践。

如果是基于Utility的扩展,比如对于扩展的api——APIStartVmInstanceExMsg。由上游发送http request时,将指定v2版本的agent。比如原有start vm会发送至path:AGENT_IP:7070/vm/start;而如果我们增强了这部分逻辑,将这段代码copy至vm_plugin_ex.py,并注册一个path,ex/vm/start。当然port也要重新注册一个,就像这样::AGENT_IP:7071/ex/vm/start

同样的,对linux.py扩展时,复制一个linux2.py来存放属于我们自己的扩展逻辑。