Skip to content

Commit 59d11f0

Browse files
bustezeroMatheMatrix
authored andcommitted
<feature>[qga]: QGA Improvements IPv6 and Expanded OS Support
1.Add IPv6 vm config support 2.Read the IPv6 address from the VM. 3.Support additional guest operating systems, such as Centos6, Ubuntu16, debian, etc. 4.More bugfix Resolves: ZSTAC-56243 Change-Id: I6868776c277a28fd38ad45eb9014d64a3931b6d0
1 parent 1813a3b commit 59d11f0

File tree

3 files changed

+147
-29
lines changed

3 files changed

+147
-29
lines changed

kvmagent/kvmagent/plugins/qga_zwatch.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class ZWatchMetricMonitor(kvmagent.KvmAgent):
3030
CONFIG_ZWATCH_METRIC_MONITOR = "/host/zwatchMetricMonitor/config"
3131

3232
ZWATCH_RESTART_CMD = "/bin/systemctl restart zwatch-vm-agent.service"
33+
ZWATCH_RESTART_CMD_EL6 = "service zwatch-vm-agent restart"
3334
ZWATCH_VM_INFO_PATH = "/var/log/zstack/vm.info"
3435
ZWATCH_VM_METRIC_PATH = "/var/log/zstack/vm_metrics.prom"
3536
ZWATCH_GET_NIC_INFO_PATH = "/usr/local/zstack/zs-tools/nic_info_linux.sh"
@@ -115,7 +116,10 @@ def qga_get_vm_nic(self, uuid, qga):
115116
nicInfoStatus = qga.guest_file_is_exist(zwatch_nic_info_path)
116117
if not nicInfoStatus:
117118
return
118-
nicInfo = qga.guest_exec_cmd_no_exitcode(zwatch_nic_info_path)
119+
if is_windows_2008(qga):
120+
nicInfo = get_nic_info_for_windows_2008(uuid, qga)
121+
else:
122+
nicInfo = qga.guest_exec_cmd_no_exitcode(zwatch_nic_info_path)
119123
nicInfo = str(nicInfo).strip()
120124
need_update = False
121125
if not self.vm_nic_info.get(uuid):
@@ -144,6 +148,9 @@ def zwatch_qga_monitor_vm(self, uuid, qga):
144148
zwatch_vm_info_path = self.ZWATCH_VM_INFO_PATH
145149
zwatch_vm_metric_path = self.ZWATCH_VM_METRIC_PATH
146150
zwatch_restart_cmd = self.ZWATCH_RESTART_CMD
151+
# centos version 6.x need special cmd
152+
if qga.os_version == '6':
153+
zwatch_restart_cmd = self.ZWATCH_RESTART_CMD_EL6
147154
dhcpStatus = not qga.guest_file_is_exist(zwatch_vm_info_path)
148155
_, qgaZWatch = qga.guest_file_read(zwatch_vm_info_path)
149156
# skip when dhcp enable
@@ -309,3 +316,50 @@ def push_metrics_to_gateway(url, uuid, metrics):
309316
}
310317
rsp = http.json_post(url, body=metrics, headers=headers)
311318
logger.debug('vm[%s] push metric with rsp[%s]' % (uuid, rsp))
319+
320+
321+
def is_windows_2008(qga):
322+
return qga.os and 'mswindows' in qga.os and '2008r2' in qga.os_version
323+
324+
325+
def subnet_mask_to_prefix_length(mask):
326+
return sum(bin(int(x)).count('1') for x in mask.split('.'))
327+
328+
329+
def get_nic_info_for_windows_2008(uuid, qga):
330+
exitcode, ret_data = qga.guest_exec_wmic(
331+
"nicconfig where IPEnabled=True get InterfaceIndex, IPaddress, IPSubnet, MACAddress /FORMAT:csv")
332+
if exitcode != 0:
333+
logger.debug('vm[%s] get nic info failed: %s' % (uuid, ret_data))
334+
return None
335+
336+
lines = ret_data.replace('\r', '').strip().split('\n')
337+
mac_to_ip = {}
338+
for line in lines:
339+
logger.debug('vm[%s] get nic info line: [%s]' % (uuid, line))
340+
columns = line.split(',')
341+
if len(columns) < 5:
342+
logger.debug('vm[%s] skipping line: [%s]' % (uuid, line))
343+
continue
344+
else:
345+
raw_ip_addresses = columns[2].strip('{}').split(';')
346+
raw_ip_subnets = columns[3].strip('{}').split(';')
347+
mac_address = columns[4].strip().lower()
348+
349+
if not len(mac_address.split(':')) == 6:
350+
continue
351+
352+
ip_addresses_with_subnets = []
353+
for ip, subnet in zip(raw_ip_addresses, raw_ip_subnets):
354+
if '.' in subnet: # Check if this is an IPv4 subnet mask
355+
prefix_length = subnet_mask_to_prefix_length(subnet)
356+
ip_addresses_with_subnets.append("{}/{}".format(ip, prefix_length))
357+
else: # Assume this is an IPv6 subnet in prefix length format
358+
ip_addresses_with_subnets.append("{}/{}".format(ip, subnet))
359+
360+
mac_to_ip[mac_address] = ip_addresses_with_subnets
361+
362+
mac_to_ip_json = json.dumps(mac_to_ip, indent=4)
363+
logger.debug('vm[%s] get nic info all: [%s]' % (uuid, mac_to_ip_json))
364+
return mac_to_ip_json
365+

kvmagent/kvmagent/plugins/vm_config.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,20 @@ class VmConfigPlugin(kvmagent.KvmAgent):
142142
VM_QGA_PARAM_FILE = "/usr/local/zstack/zs-nics.json"
143143
VM_QGA_CONFIG_LINUX_CMD = "/usr/local/zstack/zs-tools/config_linux.py"
144144
VM_QGA_SET_HOSTNAME = "/usr/local/zstack/zs-tools/set_hostname_linux.py"
145+
VM_QGA_SET_HOSTNAME_EL6 = "/usr/local/zstack/zs-tools/set_hostname_linux_el6.py"
145146
VM_CONFIG_SYNC_OS_VERSION_SUPPORT = {
146-
VmQga.VM_OS_LINUX_CENTOS: ("7", "8"),
147-
VmQga.VM_OS_LINUX_KYLIN: ("v10",),
147+
VmQga.VM_OS_LINUX_CENTOS: ("6", "7", "8"),
148+
VmQga.VM_OS_LINUX_KYLIN: ("4", "v10",),
148149
VmQga.VM_OS_LINUX_UOS: ("20",),
149150
VmQga.VM_OS_LINUX_OPEN_SUSE: ("12", "15",),
150151
VmQga.VM_OS_LINUX_SUSE_S: ("12", "15",),
151152
VmQga.VM_OS_LINUX_SUSE_D: ("12", "15",),
152153
VmQga.VM_OS_LINUX_ORACLE: ("7",),
153154
VmQga.VM_OS_LINUX_REDHAT: ("7",),
154-
VmQga.VM_OS_LINUX_UBUNTU: ("18",),
155-
VmQga.VM_OS_WINDOWS: ("10", "2012", "2012r2", "2016", "2019",)
155+
VmQga.VM_OS_LINUX_UBUNTU: ("14", "16", "18",),
156+
VmQga.VM_OS_LINUX_DEBIAN: ("9", "10",),
157+
VmQga.VM_OS_LINUX_FEDORA: ("30", "31",),
158+
VmQga.VM_OS_WINDOWS: ("10", "2012", "2012r2", "2016", "2019", "2008r2",)
156159
}
157160

158161
@lock.lock('config_vm_by_qga')
@@ -212,7 +215,10 @@ def set_vm_hostname_by_qga(self, domain, hostname, default_ip):
212215
return ret, msg
213216

214217
# exec qga command
215-
cmd_file = self.VM_QGA_SET_HOSTNAME
218+
if qga.os_version == '6':
219+
cmd_file = self.VM_QGA_SET_HOSTNAME_EL6
220+
else:
221+
cmd_file = self.VM_QGA_SET_HOSTNAME
216222
ret, msg = qga.guest_exec_python(cmd_file, [hostname, default_ip])
217223
if ret != 0:
218224
logger.debug("set vm hostname {} by qga failed: {}".format(vm_uuid, msg))

zstacklib/zstacklib/utils/qga.py

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@
4545
qga_channel_state_connected = 'connected'
4646
qga_channel_state_disconnected = 'disconnected'
4747

48+
encodings = ['utf-8', 'GB2312', 'ISO-8859-1']
49+
50+
51+
def decode_with_fallback(encoded_bytes):
52+
for encoding in encodings:
53+
try:
54+
return encoded_bytes.decode(encoding).encode('utf-8')
55+
except UnicodeDecodeError:
56+
continue
57+
raise UnicodeDecodeError("Unable to decode bytes using provided encodings")
58+
4859

4960
def get_qga_channel_state(vm_dom):
5061
xml_tree = ET.fromstring(vm_dom.XMLDesc())
@@ -61,9 +72,11 @@ def is_qga_connected(vm_dom):
6172
except:
6273
return False
6374

75+
6476
# windows zs-tools command wait 120s
6577
zs_tools_wait_retry = 120
6678

79+
6780
class QgaException(Exception):
6881
""" The base exception class for all exceptions this agent raises."""
6982

@@ -85,6 +98,8 @@ class VmQga(object):
8598
VM_OS_LINUX_SUSE_D = "sled"
8699
VM_OS_LINUX_ORACLE = "ol"
87100
VM_OS_LINUX_REDHAT = "rhel"
101+
VM_OS_LINUX_DEBIAN = "debian"
102+
VM_OS_LINUX_FEDORA = "fedora"
88103
VM_OS_WINDOWS = "mswindows"
89104

90105
ZS_TOOLS_PATN_WIN = "C:\Program Files\GuestTools\zs-tools\zs-tools.exe"
@@ -202,9 +217,9 @@ def guest_exec_bash(self, cmd, output=True, wait=qga_exec_wait_interval, retry=q
202217
exit_code = ret.get('exitcode')
203218
ret_data = None
204219
if 'out-data' in ret:
205-
ret_data = ret['out-data']
220+
ret_data = decode_with_fallback(ret['out-data'])
206221
elif 'err-data' in ret:
207-
ret_data = ret['err-data']
222+
ret_data = decode_with_fallback(ret['err-data'])
208223

209224
return exit_code, ret_data
210225

@@ -247,9 +262,9 @@ def guest_exec_python(self, file, params=None, output=True, wait=qga_exec_wait_i
247262
exit_code = ret.get('exitcode')
248263
ret_data = None
249264
if 'out-data' in ret:
250-
ret_data = ret['out-data']
265+
ret_data = decode_with_fallback(ret['out-data'])
251266
elif 'err-data' in ret:
252-
ret_data = ret['err-data']
267+
ret_data = decode_with_fallback(ret['err-data'])
253268

254269
return exit_code, ret_data
255270

@@ -264,9 +279,10 @@ def guest_exec_zs_tools(self, operate, config, output=True, wait=qga_exec_wait_i
264279
raise Exception('qga exec zs-tools unknow operate {} for vm {}'.format(operate, self.vm_uuid))
265280

266281
if ret and "pid" in ret:
267-
pid = ret["pid"]
282+
pid = ret["pid"]
268283
else:
269-
raise Exception('qga exec zs-tools operate {} config {} failed for vm {}'.format(operate, config, self.vm_uuid))
284+
raise Exception(
285+
'qga exec zs-tools operate {} config {} failed for vm {}'.format(operate, config, self.vm_uuid))
270286

271287
ret = None
272288
for i in range(retry):
@@ -276,17 +292,52 @@ def guest_exec_zs_tools(self, operate, config, output=True, wait=qga_exec_wait_i
276292
break
277293

278294
if not ret or not ret.get('exited'):
279-
raise Exception('qga exec zs-tools operate {} config {} timeout for vm {}'.format(operate, config, self.vm_uuid))
295+
raise Exception(
296+
'qga exec zs-tools operate {} config {} timeout for vm {}'.format(operate, config, self.vm_uuid))
280297

281298
exit_code = ret.get('exitcode')
282299
ret_data = None
283300
if 'out-data' in ret:
284-
ret_data = ret['out-data'].decode('utf-8').encode('utf-8')
301+
ret_data = decode_with_fallback(ret['out-data'])
285302
elif 'err-data' in ret:
286-
ret_data = ret['err-data'].decode('utf-8').encode('utf-8')
303+
ret_data = decode_with_fallback(ret['err-data'])
287304

288305
return exit_code, ret_data.replace('\r\n', '')
289306

307+
def guest_exec_wmic(self, cmd, output=True, wait=qga_exec_wait_interval, retry=qga_exec_wait_retry):
308+
cmd_parts = cmd.split('|')
309+
cmd = "{}".format(" ".join([part for part in cmd_parts]))
310+
311+
ret = self.guest_exec(
312+
{"path": "wmic", "arg": cmd.split(" "), "capture-output": output})
313+
if ret and "pid" in ret:
314+
pid = ret["pid"]
315+
else:
316+
raise Exception('qga exec cmd {} failed for vm {}'.format(cmd, self.vm_uuid))
317+
318+
if not output:
319+
logger.debug("run qga wmic: {} failed, no output".format(cmd))
320+
return 0, None
321+
322+
ret = None
323+
for i in range(retry):
324+
time.sleep(wait)
325+
ret = self.guest_exec_status(pid)
326+
if ret['exited']:
327+
break
328+
329+
if not ret or not ret.get('exited'):
330+
raise Exception('qga exec cmd {} timeout for vm {}'.format(cmd, self.vm_uuid))
331+
332+
exit_code = ret.get('exitcode')
333+
ret_data = None
334+
if 'out-data' in ret:
335+
ret_data = decode_with_fallback(ret['out-data'])
336+
elif 'err-data' in ret:
337+
ret_data = decode_with_fallback(ret['err-data'])
338+
339+
return exit_code, ret_data
340+
290341
def guest_exec_powershell(self, cmd, output=True, wait=qga_exec_wait_interval, retry=qga_exec_wait_retry):
291342
cmd_parts = cmd.split('|')
292343
cmd = "& '{}'".format("' '".join([part for part in cmd_parts]))
@@ -315,9 +366,9 @@ def guest_exec_powershell(self, cmd, output=True, wait=qga_exec_wait_interval, r
315366
exit_code = ret.get('exitcode')
316367
ret_data = None
317368
if 'out-data' in ret:
318-
ret_data = ret['out-data'].decode("GB2312")
369+
ret_data = decode_with_fallback(ret['out-data'])
319370
elif 'err-data' in ret:
320-
ret_data = ret['err-data'].decode("GB2312")
371+
ret_data = decode_with_fallback(ret['err-data'])
321372

322373
return exit_code, ret_data
323374

@@ -438,18 +489,25 @@ def guest_get_os_id_like(self):
438489
def guest_get_os_info(self):
439490
ret = self.guest_exec_bash_no_exitcode('cat /etc/os-release')
440491
if not ret:
441-
raise Exception('get os info failed')
442-
443-
lines = [line for line in ret.split('\n') if line != ""]
444-
config = {}
445-
for line in lines:
446-
if line.startswith('#'):
447-
continue
448-
449-
info = line.split('=')
450-
if len(info) != 2:
451-
continue
452-
config[info[0].strip()] = info[1].strip().strip('"')
492+
# Parse /etc/redhat-release for CentOS/RHEL 6
493+
ret = self.guest_exec_bash_no_exitcode('cat /etc/redhat-release')
494+
if not ret:
495+
raise Exception('get os info failed')
496+
parts = ret.split()
497+
if len(parts) >= 3 and parts[1] == 'release':
498+
config = {'ID': parts[0].lower(), 'VERSION_ID': parts[2]}
499+
else:
500+
# Parse /etc/os-release
501+
lines = [line for line in ret.split('\n') if line != ""]
502+
config = {}
503+
for line in lines:
504+
if line.startswith('#'):
505+
continue
506+
507+
info = line.split('=')
508+
if len(info) != 2:
509+
continue
510+
config[info[0].strip()] = info[1].strip().strip('"')
453511

454512
vm_os = config.get('ID')
455513
version = config.get('VERSION_ID')

0 commit comments

Comments
 (0)