From 05a4e60a23d319fd517b29ecfb58bdd11991d9a6 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 18 Jul 2022 16:19:56 +0200 Subject: [PATCH 01/24] python3: use six.string_types not version-dependant types Use of `unicode` needed to be immediately handled, but a few checks relying on `str` could become insufficient in python2 with the larger usage of unicode strings. Signed-off-by: Yann Dirson --- xcp/cmd.py | 3 ++- xcp/logger.py | 3 ++- xcp/net/mac.py | 3 ++- xcp/pci.py | 5 +++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/xcp/cmd.py b/xcp/cmd.py index bbd94656..fbb991d0 100644 --- a/xcp/cmd.py +++ b/xcp/cmd.py @@ -24,6 +24,7 @@ """Command processing""" import subprocess +import six import xcp.logger as logger @@ -32,7 +33,7 @@ def runCmd(command, with_stdout = False, with_stderr = False, inputtext = None): stdin = (inputtext and subprocess.PIPE or None), stdout = subprocess.PIPE, stderr = subprocess.PIPE, - shell = isinstance(command, str)) + shell = isinstance(command, six.string_types)) (out, err) = cmd.communicate(inputtext) rv = cmd.returncode diff --git a/xcp/logger.py b/xcp/logger.py index 03e8fe78..ca972b5c 100644 --- a/xcp/logger.py +++ b/xcp/logger.py @@ -33,6 +33,7 @@ import logging import logging.handlers +import six LOG = logging.getLogger() LOG.setLevel(logging.NOTSET) @@ -47,7 +48,7 @@ def openLog(lfile, level=logging.INFO): try: # if lfile is a string, assume we need to open() it - if isinstance(lfile, str): + if isinstance(lfile, six.string_types): h = open(lfile, 'a') if h.isatty(): handler = logging.StreamHandler(h) diff --git a/xcp/net/mac.py b/xcp/net/mac.py index 47e586c0..1e153c9a 100644 --- a/xcp/net/mac.py +++ b/xcp/net/mac.py @@ -31,6 +31,7 @@ __author__ = "Andrew Cooper" import re +import six VALID_COLON_MAC = re.compile(r"^([\da-fA-F]{1,2}:){5}[\da-fA-F]{1,2}$") VALID_DASH_MAC = re.compile(r"^([\da-fA-F]{1,2}-){5}[\da-fA-F]{1,2}$") @@ -59,7 +60,7 @@ def __init__(self, addr): self.octets = [] self.integer = -1 - if isinstance(addr, (str, unicode)): + if isinstance(addr, six.string_types): res = VALID_COLON_MAC.match(addr) if res: diff --git a/xcp/pci.py b/xcp/pci.py index 1c8e081d..393be196 100644 --- a/xcp/pci.py +++ b/xcp/pci.py @@ -24,6 +24,7 @@ import os.path import subprocess import re +import six _SBDF = (r"(?:(?P [\da-dA-F]{4}):)?" # Segment (optional) " (?P [\da-fA-F]{2}):" # Bus @@ -66,7 +67,7 @@ def __init__(self, addr): self.function = -1 self.index = -1 - if isinstance(addr, (str, unicode)): + if isinstance(addr, six.string_types): res = VALID_SBDFI.match(addr) if res: @@ -277,7 +278,7 @@ def findByClass(self, cls, subcls = None): class, subclass [class1, class2, ... classN]""" if subcls: - assert isinstance(cls, str) + assert isinstance(cls, six.string_types) return [x for x in self.devs.values() if x['class'] == cls and x['subclass'] == subcls] else: assert isinstance(cls, list) From 84d172a4100b0671fc1740f88a8f9ab06bd90aad Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 18 Jul 2022 18:07:43 +0200 Subject: [PATCH 02/24] python3: use "six.ensure_binary" and "six.ensure_text" for str/bytes conversion Signed-off-by: Yann Dirson --- xcp/cpiofile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xcp/cpiofile.py b/xcp/cpiofile.py index 7b2623fa..149cbcda 100755 --- a/xcp/cpiofile.py +++ b/xcp/cpiofile.py @@ -310,7 +310,7 @@ def _init_write_gz(self): self.__write(b"\037\213\010\010%s\002\377" % timestamp) if self.name.endswith(".gz"): self.name = self.name[:-3] - self.__write(self.name + NUL) + self.__write(six.ensure_binary(self.name) + NUL) def write(self, s): """Write string s to the stream. @@ -1433,7 +1433,7 @@ def extractall(self, path=".", members=None): # Set correct owner, mtime and filemode on directories. for cpioinfo in directories: - path = os.path.join(path, cpioinfo.name) + path = os.path.join(path, six.ensure_text(cpioinfo.name)) try: self.chown(cpioinfo, path) self.utime(cpioinfo, path) From 7ac16beaa4cfc35c7842a863246397861ce2e8a6 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 25 May 2007 20:17:15 +0000 Subject: [PATCH 03/24] Remove direct call's to file's constructor and replace them with calls to open() as ths is considered best practice. (cherry picked from cpython commit 6cef076ba5edbfa42239924951d8acbb087b3b19) Signed-off-by: Yann Dirson --- xcp/cpiofile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/xcp/cpiofile.py b/xcp/cpiofile.py index 149cbcda..0ddaff6c 100755 --- a/xcp/cpiofile.py +++ b/xcp/cpiofile.py @@ -951,7 +951,7 @@ def __init__(self, name=None, mode="r", fileobj=None): self.mode = {"r": "rb", "a": "r+b", "w": "wb"}[mode] if not fileobj: - fileobj = file(name, self.mode) + fileobj = bltn_open(name, self.mode) self._extfileobj = False else: if name is None and hasattr(fileobj, "name"): @@ -1109,7 +1109,7 @@ def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9): raise CompressionError("gzip module is not available") if fileobj is None: - fileobj = file(name, mode + "b") + fileobj = bltn_open(name, mode + "b") try: t = cls.cpioopen(name, mode, gzip.GzipFile(name, mode, compresslevel, fileobj)) @@ -1354,7 +1354,7 @@ def add(self, name, arcname=None, recursive=True): # Append the cpio header and data to the archive. if cpioinfo.isreg(): - f = file(name, "rb") + f = bltn_open(name, "rb") self.addfile(cpioinfo, f) f.close() @@ -1594,7 +1594,7 @@ def makefile(self, cpioinfo, targetpath): if extractinfo: source = self.extractfile(extractinfo) - target = file(targetpath, "wb") + target = bltn_open(targetpath, "wb") copyfileobj(source, target) source.close() target.close() @@ -1926,5 +1926,5 @@ def is_cpiofile(name): except CpioError: return False -def cpioOpen(*al, **ad): - return CpioFile.open(*al, **ad) +bltn_open = open +open = CpioFile.open From 520d419c6cd2f2e57ca9628457bd363a50d1d11c Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 18 Jul 2022 18:15:22 +0200 Subject: [PATCH 04/24] python3: xcp.net.mac: use six.python_2_unicode_compatible for stringification Signed-off-by: Yann Dirson --- xcp/net/mac.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xcp/net/mac.py b/xcp/net/mac.py index 1e153c9a..c0d4fba0 100644 --- a/xcp/net/mac.py +++ b/xcp/net/mac.py @@ -37,6 +37,7 @@ VALID_DASH_MAC = re.compile(r"^([\da-fA-F]{1,2}-){5}[\da-fA-F]{1,2}$") VALID_DOTQUAD_MAC = re.compile(r"^([\da-fA-F]{1,4}\.){2}[\da-fA-F]{1,4}$") +@six.python_2_unicode_compatible class MAC(object): """ Mac address object for manipulation and comparison @@ -123,9 +124,6 @@ def is_local(self): def __str__(self): - return unicode(self).encode('utf-8') - - def __unicode__(self): return ':'.join([ "%0.2x" % x for x in self.octets]) def __repr__(self): From a59c3ba385d48e5ded9412ebc8c687dd585014bf Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 18 Jul 2022 18:21:17 +0200 Subject: [PATCH 05/24] xcp.net.ifrename.logic: use "logger.warning", "logger.warn" is deprecated Signed-off-by: Yann Dirson --- xcp/net/ifrename/logic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/xcp/net/ifrename/logic.py b/xcp/net/ifrename/logic.py index e2a3484b..1aa534c0 100644 --- a/xcp/net/ifrename/logic.py +++ b/xcp/net/ifrename/logic.py @@ -289,9 +289,10 @@ def rename_logic( static_rules, # Check that the function still has the same number of nics if len(lastnics) != len(newnics): - LOG.warn("multi-nic function %s had %d nics but now has %d. " - "Defering all until later for renaming" - % (fn, len(lastnics), len(newnics))) + LOG.warning( + "multi-nic function %s had %d nics but now has %d. " + "Defering all until later for renaming", + fn, len(lastnics), len(newnics)) continue # Check that all nics are still pending a rename From 0832486eb585081309ee384ba9ebe0199178d6fa Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 18 Jul 2022 18:22:58 +0200 Subject: [PATCH 06/24] python3: use raw strings for regexps, fixes insufficient quoting Running tests on python3 did reveal some of them. Signed-off-by: Yann Dirson --- xcp/bootloader.py | 6 +++--- xcp/dom0.py | 2 +- xcp/net/ifrename/logic.py | 6 +++--- xcp/net/ifrename/static.py | 2 +- xcp/pci.py | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/xcp/bootloader.py b/xcp/bootloader.py index a1d19709..81a61bd4 100644 --- a/xcp/bootloader.py +++ b/xcp/bootloader.py @@ -336,19 +336,19 @@ def create_label(title): try: for line in fh: l = line.strip() - menu_match = re.match("menuentry ['\"]([^']*)['\"](.*){", l) + menu_match = re.match(r"menuentry ['\"]([^']*)['\"](.*){", l) # Only parse unindented default and timeout lines to prevent # changing these lines in if statements. if l.startswith('set default=') and l == line.rstrip(): default = l.split('=')[1] - match = re.match("['\"](.*)['\"]$", default) + match = re.match(r"['\"](.*)['\"]$", default) if match: default = match.group(1) elif l.startswith('set timeout=') and l == line.rstrip(): timeout = int(l.split('=')[1]) * 10 elif l.startswith('serial'): - match = re.match("serial --unit=(\d+) --speed=(\d+)", l) + match = re.match(r"serial --unit=(\d+) --speed=(\d+)", l) if match: serial = { 'port': int(match.group(1)), diff --git a/xcp/dom0.py b/xcp/dom0.py index 086a683b..b8a46c3a 100644 --- a/xcp/dom0.py +++ b/xcp/dom0.py @@ -96,7 +96,7 @@ def default_memory(host_mem_kib): return default_memory_for_version(host_mem_kib, platform_version) -_size_and_unit_re = re.compile("^(-?\d+)([bkmg]?)$", re.IGNORECASE) +_size_and_unit_re = re.compile(r"^(-?\d+)([bkmg]?)$", re.IGNORECASE) def _parse_size_and_unit(s): m = _size_and_unit_re.match(s) diff --git a/xcp/net/ifrename/logic.py b/xcp/net/ifrename/logic.py index 1aa534c0..41e74c02 100644 --- a/xcp/net/ifrename/logic.py +++ b/xcp/net/ifrename/logic.py @@ -52,9 +52,9 @@ from xcp.logger import LOG from xcp.net.ifrename.macpci import MACPCI -VALID_CUR_STATE_KNAME = re.compile("^(?:eth[\d]+|side-[\d]+-eth[\d]+)$") -VALID_ETH_NAME = re.compile("^eth([\d])+$") -VALID_IBFT_NAME = re.compile("^ibft([\d])+$") +VALID_CUR_STATE_KNAME = re.compile(r"^(?:eth[\d]+|side-[\d]+-eth[\d]+)$") +VALID_ETH_NAME = re.compile(r"^eth([\d])+$") +VALID_IBFT_NAME = re.compile(r"^ibft([\d])+$") # util needs to import VALID_ETH_NAME from xcp.net.ifrename import util diff --git a/xcp/net/ifrename/static.py b/xcp/net/ifrename/static.py index c1b9b100..bf503d54 100644 --- a/xcp/net/ifrename/static.py +++ b/xcp/net/ifrename/static.py @@ -89,7 +89,7 @@ class StaticRules(object): methods = ["mac", "pci", "ppn", "label", "guess"] validators = { "mac": VALID_MAC, "pci": VALID_PCI, - "ppn": re.compile("^(?:em\d+|p(?:ci)?\d+p\d+)$") + "ppn": re.compile(r"^(?:em\d+|p(?:ci)?\d+p\d+)$") } def __init__(self, path=None, fd=None): diff --git a/xcp/pci.py b/xcp/pci.py index 393be196..d6cb4fef 100644 --- a/xcp/pci.py +++ b/xcp/pci.py @@ -27,9 +27,9 @@ import six _SBDF = (r"(?:(?P [\da-dA-F]{4}):)?" # Segment (optional) - " (?P [\da-fA-F]{2}):" # Bus - " (?P [\da-fA-F]{2})\." # Device - " (?P[\da-fA-F])" # Function + r" (?P [\da-fA-F]{2}):" # Bus + r" (?P [\da-fA-F]{2})\." # Device + r" (?P[\da-fA-F])" # Function ) # Don't change the meaning of VALID_SBDF as some parties may be using it @@ -37,7 +37,7 @@ VALID_SBDFI = re.compile( r"^(?P%s)" - " (?:[[](?P[\d]{1,2})[]])?$" # Index (optional) + r" (?:[[](?P[\d]{1,2})[]])?$" # Index (optional) % _SBDF , re.X) From 7e14499e55ee8f5684dc4e9b2ad01ee441e8000d Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 19 Jul 2022 12:06:59 +0200 Subject: [PATCH 07/24] test_dom0: mock "open()" in a python3-compatible way Signed-off-by: Yann Dirson --- tests/test_dom0.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_dom0.py b/tests/test_dom0.py index bf198341..440bb109 100644 --- a/tests/test_dom0.py +++ b/tests/test_dom0.py @@ -30,7 +30,7 @@ def mock_version(open_mock, version): (2*1024, 4*1024, 8*1024), # Above max ] - with patch("__builtin__.open") as open_mock: + with patch("xcp.dom0.open") as open_mock: for host_gib, dom0_mib, _ in test_values: mock_version(open_mock, '2.8.0') expected = dom0_mib * 1024; @@ -39,7 +39,7 @@ def mock_version(open_mock, version): open_mock.assert_called_with("/etc/xensource-inventory") - with patch("__builtin__.open") as open_mock: + with patch("xcp.dom0.open") as open_mock: for host_gib, _, dom0_mib in test_values: mock_version(open_mock, '2.9.0') expected = dom0_mib * 1024; From 9c64243c67c37f8c7cbc33a0edf8244f5613dd22 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 19 Jul 2022 15:05:25 +0200 Subject: [PATCH 08/24] ifrename: don't rely on dict ordering in tests There is no guaranty about ordering of dict elements, and tests compare results derived from enumerating a dict element. We could have used an OrderedDict to store the formulae and get a predictible output order, but just considering the output as a set seems better. Only applying this to rules expected to hold more than one element. Signed-off-by: Yann Dirson --- tests/test_ifrename_dynamic.py | 4 ++-- tests/test_ifrename_static.py | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_ifrename_dynamic.py b/tests/test_ifrename_dynamic.py index 1cc95e39..0948d254 100644 --- a/tests/test_ifrename_dynamic.py +++ b/tests/test_ifrename_dynamic.py @@ -125,10 +125,10 @@ def test_pci_matching_invert(self): MACPCI("c8:cb:b8:d3:0c:cf", "0000:04:00.0", kname="eth1", ppn="", label="")]) - self.assertEqual(dr.rules,[ + self.assertEqual(set(dr.rules), set([ MACPCI("c8:cb:b8:d3:0c:ce", "0000:04:00.0", tname="eth1"), MACPCI("c8:cb:b8:d3:0c:cf", "0000:04:00.0", tname="eth0") - ]) + ])) def test_pci_missing(self): diff --git a/tests/test_ifrename_static.py b/tests/test_ifrename_static.py index 9b11e380..674decfe 100644 --- a/tests/test_ifrename_static.py +++ b/tests/test_ifrename_static.py @@ -375,10 +375,10 @@ def test_pci_matching(self): sr.generate(self.state) - self.assertEqual(sr.rules,[ - MACPCI("c8:cb:b8:d3:0c:cf", "0000:04:00.0", tname="eth1"), - MACPCI("c8:cb:b8:d3:0c:ce", "0000:04:00.0", tname="eth0") - ]) + self.assertEqual(set(sr.rules), set([ + MACPCI("c8:cb:b8:d3:0c:cf", "0000:04:00.0", tname="eth1"), + MACPCI("c8:cb:b8:d3:0c:ce", "0000:04:00.0", tname="eth0") + ])) def test_pci_matching_invert(self): @@ -389,10 +389,10 @@ def test_pci_matching_invert(self): sr.generate(self.state) - self.assertEqual(sr.rules,[ - MACPCI("c8:cb:b8:d3:0c:ce", "0000:04:00.0", tname="eth1"), - MACPCI("c8:cb:b8:d3:0c:cf", "0000:04:00.0", tname="eth0") - ]) + self.assertEqual(set(sr.rules), set([ + MACPCI("c8:cb:b8:d3:0c:ce", "0000:04:00.0", tname="eth1"), + MACPCI("c8:cb:b8:d3:0c:cf", "0000:04:00.0", tname="eth0") + ])) def test_pci_matching_mixed(self): @@ -403,10 +403,10 @@ def test_pci_matching_mixed(self): sr.generate(self.state) - self.assertEqual(sr.rules,[ - MACPCI("c8:cb:b8:d3:0c:cf", "0000:04:00.0", tname="eth0"), - MACPCI("c8:cb:b8:d3:0c:ce", "0000:04:00.0", tname="eth1") - ]) + self.assertEqual(set(sr.rules), set([ + MACPCI("c8:cb:b8:d3:0c:cf", "0000:04:00.0", tname="eth0"), + MACPCI("c8:cb:b8:d3:0c:ce", "0000:04:00.0", tname="eth1") + ])) def test_pci_missing(self): From ae7907835ab213e52c14a4f2573b9ad1cb17ad08 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 20 Jul 2022 16:18:39 +0200 Subject: [PATCH 09/24] test_cpio: ensure paths are handled as text Caught by extended test. Signed-off-by: Yann Dirson --- xcp/cpiofile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xcp/cpiofile.py b/xcp/cpiofile.py index 0ddaff6c..50852c09 100755 --- a/xcp/cpiofile.py +++ b/xcp/cpiofile.py @@ -1420,7 +1420,7 @@ def extractall(self, path=".", members=None): # Extract directory with a safe mode, so that # all files below can be extracted as well. try: - os.makedirs(os.path.join(path, cpioinfo.name), 0o777) + os.makedirs(os.path.join(path, six.ensure_text(cpioinfo.name)), 0o777) except EnvironmentError: pass directories.append(cpioinfo) @@ -1462,7 +1462,7 @@ def extract(self, member, path=""): cpioinfo._link_path = path try: - self._extract_member(cpioinfo, os.path.join(path, cpioinfo.name)) + self._extract_member(cpioinfo, os.path.join(path, six.ensure_text(cpioinfo.name))) except EnvironmentError as e: if self.errorlevel > 0: raise From 346ebc091cf8f3f3cb0cadd6abfc93188df4f9f5 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 26 Jul 2022 17:16:30 +0200 Subject: [PATCH 10/24] cpiofile: migrate last "list.sort()" call still using a "cmp" argument This goes away in python3. Signed-off-by: Yann Dirson --- xcp/cpiofile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xcp/cpiofile.py b/xcp/cpiofile.py index 50852c09..fb4d96f7 100755 --- a/xcp/cpiofile.py +++ b/xcp/cpiofile.py @@ -1428,7 +1428,7 @@ def extractall(self, path=".", members=None): self.extract(cpioinfo, path) # Reverse sort directories. - directories.sort(lambda a, b: cmp(a.name, b.name)) + directories.sort(key=lambda x: x.name) directories.reverse() # Set correct owner, mtime and filemode on directories. From 5fd6cdf33389d09a7dacaba263a7fd46d302ab7d Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 25 Jul 2022 12:32:56 +0200 Subject: [PATCH 11/24] WIP python3: fix xmlunwrap and its test to align with the use of bytes FIXME: I'm quite unsure why xcp.xmlunwrap would want to use bytes and not unicode strings, but the encode/decode calls make it quite clear it wants to work with bytes. That makes the API painful to use in python3. --- tests/test_xmlunwrap.py | 12 ++++++------ xcp/xmlunwrap.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_xmlunwrap.py b/tests/test_xmlunwrap.py index f71d778f..23313331 100644 --- a/tests/test_xmlunwrap.py +++ b/tests/test_xmlunwrap.py @@ -18,18 +18,18 @@ def test(self): self.assertEqual([getText(el) for el in getElementsByTagName(self.top_el, ["fred"])], - ["text1", "text2"]) + [b"text1", b"text2"]) - x = getMapAttribute(self.top_el, ["mode"], [('test', 42), ('stuff', 77)]) + x = getMapAttribute(self.top_el, ["mode"], [(b'test', 42), (b'stuff', 77)]) self.assertEqual(x, 42) - x = getMapAttribute(self.top_el, ["made"], [('test', 42), ('stuff', 77)], - default='stuff') + x = getMapAttribute(self.top_el, ["made"], [(b'test', 42), (b'stuff', 77)], + default=b'stuff') self.assertEqual(x, 77) x = getStrAttribute(self.top_el, ["mode"]) - self.assertEqual(x, "test") + self.assertEqual(x, b"test") x = getStrAttribute(self.top_el, ["made"]) - self.assertEqual(x, "") + self.assertEqual(x, b"") x = getStrAttribute(self.top_el, ["made"], None) self.assertEqual(x, None) diff --git a/xcp/xmlunwrap.py b/xcp/xmlunwrap.py index 1487afab..2832b680 100644 --- a/xcp/xmlunwrap.py +++ b/xcp/xmlunwrap.py @@ -44,11 +44,11 @@ def getElementsByTagName(el, tags, mandatory = False): raise XmlUnwrapError("Missing mandatory element %s" % tags[0]) return matching -def getStrAttribute(el, attrs, default = '', mandatory = False): +def getStrAttribute(el, attrs, default=b'', mandatory=False): matching = [] for attr in attrs: val = el.getAttribute(attr).encode() - if val != '': + if val != b'': matching.append(val) if len(matching) == 0: if mandatory: From 67225f7d11ccf84b6a892516ffe029beda8b0c19 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 26 Jul 2022 16:32:02 +0200 Subject: [PATCH 12/24] xcp.repository: switch from md5 to hashlib.md5 hashlib came with python 2.5, and old md5 module disappears in 3.0 Signed-off-by: Yann Dirson --- xcp/repository.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xcp/repository.py b/xcp/repository.py index ca284647..fdaefa5e 100644 --- a/xcp/repository.py +++ b/xcp/repository.py @@ -23,7 +23,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import md5 +from hashlib import md5 import os.path import xml.dom.minidom import ConfigParser @@ -246,7 +246,7 @@ def findRepositories(cls, access): def __init__(self, access, base, is_group = False): BaseRepository.__init__(self, access, base) self.is_group = is_group - self._md5 = md5.new() + self._md5 = md5() self.requires = [] self.packages = [] @@ -288,7 +288,7 @@ def _parse_repofile(self, repofile): repofile.close() # update md5sum for repo - self._md5.update(repofile_contents) + self._md5.update(repofile_contents.encode()) # build xml doc object try: From 27fdfe941a231429b17084c98d8406eba2f60d98 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 26 Jul 2022 16:32:18 +0200 Subject: [PATCH 13/24] WIP xcp.repository: switch from ConfigParser to configparser This is supposed to be just a module renaming to conform to PEP8, see https://docs.python.org/3/whatsnew/3.0.html#library-changes The SafeConfigParser class has been renamed to ConfigParser in Python 3.2, and backported as addon package. The `readfp` method now triggers a deprecation warning to replace it with `read_file`. FIXME: With python3 some Accessor implementations (e.g. FileAccessor) provide a text stream for repository config (and with python2 all implementations), while others (e.g. HTTPAccessor) provide a binary stream. But on python3 ConfigParser will bomb out if given a binary stream, so use a TextIOWrapper to access the config. This is a hack, which cannot be used when it is binary data which has to be read (see later commits), so I don't consider this commit to be correct in that respect. --- requirements-dev.txt | 3 +++ tests/test_accessor.py | 2 +- tests/test_repository.py | 2 +- xcp/repository.py | 19 ++++++++++++++----- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b2799686..84da09b5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,3 +8,6 @@ pytest-cov # dependencies also in setup.py until they can be used six future + +# python-2.7 only +configparser ; python_version < "3.0" diff --git a/tests/test_accessor.py b/tests/test_accessor.py index ade787e6..8c892845 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -4,7 +4,7 @@ class TestAccessor(unittest.TestCase): def test_http(self): - raise unittest.SkipTest("comment out if you really mean it") + #raise unittest.SkipTest("comment out if you really mean it") a = xcp.accessor.createAccessor("https://updates.xcp-ng.org/netinstall/8.2.1", True) a.start() self.assertTrue(a.access('.treeinfo')) diff --git a/tests/test_repository.py b/tests/test_repository.py index 833627d0..4768740d 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -6,7 +6,7 @@ class TestRepository(unittest.TestCase): def test_http(self): - raise unittest.SkipTest("comment out if you really mean it") + #raise unittest.SkipTest("comment out if you really mean it") a = xcp.accessor.createAccessor("https://updates.xcp-ng.org/netinstall/8.2.1", True) repo_ver = repository.BaseRepository.getRepoVer(a) self.assertEqual(repo_ver, Version([3, 2, 1])) diff --git a/xcp/repository.py b/xcp/repository.py index fdaefa5e..35a2c08d 100644 --- a/xcp/repository.py +++ b/xcp/repository.py @@ -24,9 +24,11 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from hashlib import md5 +import io import os.path import xml.dom.minidom -import ConfigParser +import configparser +import sys import six @@ -179,10 +181,17 @@ def _getVersion(cls, access, category): access.start() try: - treeinfofp = access.openAddress(cls.TREEINFO_FILENAME) - treeinfo = ConfigParser.SafeConfigParser() - treeinfo.readfp(treeinfofp) - treeinfofp.close() + rawtreeinfofp = access.openAddress(cls.TREEINFO_FILENAME) + if sys.version_info < (3, 0) or isinstance(rawtreeinfofp, io.TextIOBase): + # e.g. with FileAccessor + treeinfofp = rawtreeinfofp + else: + # e.g. with HTTPAccessor + treeinfofp = io.TextIOWrapper(rawtreeinfofp, encoding='utf-8') + treeinfo = configparser.ConfigParser() + treeinfo.read_file(treeinfofp) + treeinfofp = None + rawtreeinfofp.close() if treeinfo.has_section('system-v1'): ver_str = treeinfo.get('system-v1', category_map[category]) else: From b004cab48ca683d29848f1285592bad259d349e9 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 26 Sep 2022 14:18:59 +0200 Subject: [PATCH 14/24] test: use parametrized tests Testing several accessor classes causes code duplication, which can be avoided with help from the `parametrized` package (unfortunately, `pytest` support cannot be used together with `unittest`). Not a big deal right now, but starts becoming painful when adding new tests or testing other Accessor classes. Signed-off-by: Yann Dirson --- requirements-dev.txt | 1 + tests/test_accessor.py | 16 +++++----------- tests/test_repository.py | 17 +++++------------ 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 84da09b5..4debd01d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,7 @@ diff_cover mock pytest pytest-cov +parameterized # dependencies also in setup.py until they can be used six future diff --git a/tests/test_accessor.py b/tests/test_accessor.py index 8c892845..e7e87a3e 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -1,19 +1,13 @@ import unittest +from parameterized import parameterized_class import xcp.accessor +@parameterized_class([{"url": "file://tests/data/repo/"}, + {"url": "https://updates.xcp-ng.org/netinstall/8.2.1"}]) class TestAccessor(unittest.TestCase): - def test_http(self): - #raise unittest.SkipTest("comment out if you really mean it") - a = xcp.accessor.createAccessor("https://updates.xcp-ng.org/netinstall/8.2.1", True) - a.start() - self.assertTrue(a.access('.treeinfo')) - self.assertFalse(a.access('no_such_file')) - self.assertEqual(a.lastError, 404) - a.finish() - - def test_file(self): - a = xcp.accessor.createAccessor("file://tests/data/repo/", True) + def test_access(self): + a = xcp.accessor.createAccessor(self.url, True) a.start() self.assertTrue(a.access('.treeinfo')) self.assertFalse(a.access('no_such_file')) diff --git a/tests/test_repository.py b/tests/test_repository.py index 4768740d..8081c33a 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -1,22 +1,15 @@ import unittest +from parameterized import parameterized_class import xcp.accessor from xcp import repository from xcp.version import Version +@parameterized_class([{"url": "file://tests/data/repo/"}, + {"url": "https://updates.xcp-ng.org/netinstall/8.2.1"}]) class TestRepository(unittest.TestCase): - def test_http(self): - #raise unittest.SkipTest("comment out if you really mean it") - a = xcp.accessor.createAccessor("https://updates.xcp-ng.org/netinstall/8.2.1", True) - repo_ver = repository.BaseRepository.getRepoVer(a) - self.assertEqual(repo_ver, Version([3, 2, 1])) - product_ver = repository.BaseRepository.getProductVersion(a) - self.assertEqual(product_ver, Version([8, 2, 1])) - repos = repository.BaseRepository.findRepositories(a) - self.assertEqual(len(repos), 1) - - def test_file(self): - a = xcp.accessor.createAccessor("file://tests/data/repo/", True) + def test_basicinfo(self): + a = xcp.accessor.createAccessor(self.url, True) repo_ver = repository.BaseRepository.getRepoVer(a) self.assertEqual(repo_ver, Version([3, 2, 1])) product_ver = repository.BaseRepository.getProductVersion(a) From cdd5ee847416fb5f76992f025b1f3f77d520edf1 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 26 Sep 2022 14:19:10 +0200 Subject: [PATCH 15/24] WIP test_accessor: check for I/O on binary files This test uses the same kind of I/O (file copy) that prepare_host_upgrade.py does. FIXME: the copy cannot proceed this way in python3 --- tests/data/repo/boot/isolinux/mboot.c32 | Bin 0 -> 33628 bytes tests/test_accessor.py | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/data/repo/boot/isolinux/mboot.c32 diff --git a/tests/data/repo/boot/isolinux/mboot.c32 b/tests/data/repo/boot/isolinux/mboot.c32 new file mode 100644 index 0000000000000000000000000000000000000000..3f5b2fe5acf672f54af1000ae844076807abb0d1 GIT binary patch literal 33628 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 L!VlPkeX#Nmn``P+ literal 0 HcmV?d00001 diff --git a/tests/test_accessor.py b/tests/test_accessor.py index e7e87a3e..dfefa00d 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -1,4 +1,7 @@ import unittest +import hashlib +from tempfile import NamedTemporaryFile + from parameterized import parameterized_class import xcp.accessor @@ -13,3 +16,23 @@ def test_access(self): self.assertFalse(a.access('no_such_file')) self.assertEqual(a.lastError, 404) a.finish() + + def test_file_binfile(self): + BINFILE = "boot/isolinux/mboot.c32" + a = xcp.accessor.createAccessor(self.url, True) + a.start() + self.assertTrue(a.access(BINFILE)) + inf = a.openAddress(BINFILE) + with NamedTemporaryFile("w") as outf: + outf.writelines(inf) + outf.flush() + hasher = hashlib.md5() + with open(outf.name, "rb") as bincontents: + while True: + data = bincontents.read() + if not data: # EOF + break + hasher.update(data) + csum = hasher.hexdigest() + self.assertEqual(csum, "eab52cebc3723863432dc672360f6dac") + a.finish() From 71183e4c21f0f2e68ec6b1006f9869a051de37a4 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 26 Sep 2022 14:19:21 +0200 Subject: [PATCH 16/24] WIP test_accessor: write into copy file as binary This works properly for the http case, but FileAccessor provides us with a text fileobj handle, and `read()` gets a UTF-8 decoding error. FIXME: Accessor ctor requires a `mode` argument --- tests/test_accessor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_accessor.py b/tests/test_accessor.py index dfefa00d..d001edb8 100644 --- a/tests/test_accessor.py +++ b/tests/test_accessor.py @@ -23,8 +23,12 @@ def test_file_binfile(self): a.start() self.assertTrue(a.access(BINFILE)) inf = a.openAddress(BINFILE) - with NamedTemporaryFile("w") as outf: - outf.writelines(inf) + with NamedTemporaryFile("wb") as outf: + while True: + data = inf.read() + if not data: # EOF + break + outf.write(data) outf.flush() hasher = hashlib.md5() with open(outf.name, "rb") as bincontents: From d6566dd7e12e922919150d10ad7fd5a648c71c9a Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 20 Jul 2022 12:06:18 +0200 Subject: [PATCH 17/24] Pylint complements: honor len-as-condition convention Signed-off-by: Yann Dirson --- tests/test_cpio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cpio.py b/tests/test_cpio.py index c3543c89..c1c4cd37 100644 --- a/tests/test_cpio.py +++ b/tests/test_cpio.py @@ -14,7 +14,7 @@ def writeRandomFile(fn, size, start=b'', add=b'a'): with open(fn, 'wb') as f: m = md5() m.update(start) - assert(len(add) != 0) + assert add while size > 0: d = m.digest() if size < len(d): From ac1b1b894b59b1c6606ea62a153c8d3fc7151e20 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 15 Jul 2022 15:40:49 +0200 Subject: [PATCH 18/24] Pylint complements: whitespace in expressions Signed-off-by: Yann Dirson --- tests/test_cpio.py | 2 +- xcp/cmd.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_cpio.py b/tests/test_cpio.py index c1c4cd37..fdb34f40 100644 --- a/tests/test_cpio.py +++ b/tests/test_cpio.py @@ -18,7 +18,7 @@ def writeRandomFile(fn, size, start=b'', add=b'a'): while size > 0: d = m.digest() if size < len(d): - d=d[:size] + d = d[:size] f.write(d) size -= len(d) m.update(add) diff --git a/xcp/cmd.py b/xcp/cmd.py index fbb991d0..16cfb64f 100644 --- a/xcp/cmd.py +++ b/xcp/cmd.py @@ -29,11 +29,11 @@ import xcp.logger as logger def runCmd(command, with_stdout = False, with_stderr = False, inputtext = None): - cmd = subprocess.Popen(command, bufsize = 1, - stdin = (inputtext and subprocess.PIPE or None), - stdout = subprocess.PIPE, - stderr = subprocess.PIPE, - shell = isinstance(command, six.string_types)) + cmd = subprocess.Popen(command, bufsize=1, + stdin=(inputtext and subprocess.PIPE or None), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=isinstance(command, six.string_types)) (out, err) = cmd.communicate(inputtext) rv = cmd.returncode From d58e988646c892528ce092527082cc03779e83cf Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 8 Aug 2022 16:24:30 +0200 Subject: [PATCH 19/24] Pylint complements: test_ifrename_logic: disable "no-member" warning Reported under python3 for members created on-the-fly in `setUp()` Signed-off-by: Yann Dirson --- tests/test_ifrename_logic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_ifrename_logic.py b/tests/test_ifrename_logic.py index 3bb7a911..a2715334 100644 --- a/tests/test_ifrename_logic.py +++ b/tests/test_ifrename_logic.py @@ -518,6 +518,7 @@ def test_ibft_nic_to_ibft(self): class TestInputSanitisation(unittest.TestCase): + # pylint: disable=no-member def setUp(self): """ From efeecfa68737a6bb8213a799bb70d828c4eec18e Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Mon, 8 Aug 2022 16:33:12 +0200 Subject: [PATCH 20/24] Pylint complements: avoid no-else-raise "refactor" issues With python3, pylint complains about `else: raise()` constructs. This rework avoids them and reduces cyclomatic complexity by using the error-out-first idiom. Signed-off-by: Yann Dirson --- xcp/net/mac.py | 32 ++++++++++++------------ xcp/pci.py | 66 ++++++++++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/xcp/net/mac.py b/xcp/net/mac.py index c0d4fba0..56ba4b7b 100644 --- a/xcp/net/mac.py +++ b/xcp/net/mac.py @@ -61,27 +61,25 @@ def __init__(self, addr): self.octets = [] self.integer = -1 - if isinstance(addr, six.string_types): - - res = VALID_COLON_MAC.match(addr) - if res: - self._set_from_str_octets(addr.split(":")) - return + if not isinstance(addr, six.string_types): + raise TypeError("String expected") - res = VALID_DASH_MAC.match(addr) - if res: - self._set_from_str_octets(addr.split("-")) - return + res = VALID_COLON_MAC.match(addr) + if res: + self._set_from_str_octets(addr.split(":")) + return - res = VALID_DOTQUAD_MAC.match(addr) - if res: - self._set_from_str_quads(addr.split(".")) - return + res = VALID_DASH_MAC.match(addr) + if res: + self._set_from_str_octets(addr.split("-")) + return - raise ValueError("Unrecognised MAC address '%s'" % addr) + res = VALID_DOTQUAD_MAC.match(addr) + if res: + self._set_from_str_quads(addr.split(".")) + return - else: - raise TypeError("String expected") + raise ValueError("Unrecognised MAC address '%s'" % addr) def _set_from_str_octets(self, octets): diff --git a/xcp/pci.py b/xcp/pci.py index d6cb4fef..1f911a6b 100644 --- a/xcp/pci.py +++ b/xcp/pci.py @@ -67,48 +67,46 @@ def __init__(self, addr): self.function = -1 self.index = -1 - if isinstance(addr, six.string_types): - - res = VALID_SBDFI.match(addr) - if res: - groups = res.groupdict() + if not isinstance(addr, six.string_types): + raise TypeError("String expected") - if "segment" in groups and groups["segment"] is not None: - self.segment = int(groups["segment"], 16) - else: - self.segment = 0 + res = VALID_SBDFI.match(addr) + if res: + groups = res.groupdict() - self.bus = int(groups["bus"], 16) - if not ( 0 <= self.bus < 2**8 ): - raise ValueError("Bus '%d' out of range 0 <= bus < 256" - % (self.bus,)) + if "segment" in groups and groups["segment"] is not None: + self.segment = int(groups["segment"], 16) + else: + self.segment = 0 - self.device = int(groups["device"], 16) - if not ( 0 <= self.device < 2**5): - raise ValueError("Device '%d' out of range 0 <= device < 32" - % (self.device,)) + self.bus = int(groups["bus"], 16) + if not ( 0 <= self.bus < 2**8 ): + raise ValueError("Bus '%d' out of range 0 <= bus < 256" + % (self.bus,)) - self.function = int(groups["function"], 16) - if not ( 0 <= self.function < 2**3): - raise ValueError("Function '%d' out of range 0 <= device " - "< 8" % (self.function,)) + self.device = int(groups["device"], 16) + if not ( 0 <= self.device < 2**5): + raise ValueError("Device '%d' out of range 0 <= device < 32" + % (self.device,)) - if "index" in groups and groups["index"] is not None: - self.index = int(groups["index"]) - else: - self.index = 0 + self.function = int(groups["function"], 16) + if not ( 0 <= self.function < 2**3): + raise ValueError("Function '%d' out of range 0 <= device " + "< 8" % (self.function,)) - self.integer = (int(self.segment << 16 | - self.bus << 8 | - self.device << 3 | - self.function) << 8 | - self.index) - return + if "index" in groups and groups["index"] is not None: + self.index = int(groups["index"]) + else: + self.index = 0 - raise ValueError("Unrecognised PCI address '%s'" % addr) + self.integer = (int(self.segment << 16 | + self.bus << 8 | + self.device << 3 | + self.function) << 8 | + self.index) + return - else: - raise TypeError("String expected") + raise ValueError("Unrecognised PCI address '%s'" % addr) def __str__(self): From 09ba68aca491f26e8552e935fe36a6af634b55a5 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 26 Jul 2022 16:50:54 +0200 Subject: [PATCH 21/24] CI: also run tests with python3 diff-cover defaults to origin/main in new version, it seems. Signed-off-by: Yann Dirson --- .github/workflows/main.yml | 32 +++++++++++++++++++------------- requirements-dev.txt | 1 + 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 53462463..adf75fd7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,17 +3,24 @@ name: Unit tests on: [push, pull_request] jobs: - test_py2: - runs-on: ubuntu-20.04 + test: + strategy: + matrix: + include: + - pyversion: '2.7' + os: ubuntu-20.04 + - pyversion: '3' + os: ubuntu-latest + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - - name: Set up Python 2.7 + - name: Set up Python ${{ matrix.pyversion }} uses: actions/setup-python@v2 with: - python-version: '2.7' + python-version: ${{ matrix.pyversion }} - name: Install dependencies run: | @@ -21,7 +28,6 @@ jobs: pip install -r requirements-dev.txt # FIXME: branding.py still has no permanent home curl https://gist.github.com/ydirson/3c36a7e19d762cc529a6c82340894ccc/raw/5ca39f621b1feab813e171f535c1aad1bd483f1d/branding.py -O -L - pip install pyliblzma pip install -e . command -v xz @@ -29,22 +35,22 @@ jobs: run: | pytest --cov -rP coverage xml - coverage html - coverage html -d htmlcov-tests --include="tests/*" - diff-cover --html-report coverage-diff.html coverage.xml + coverage html -d htmlcov-${{ matrix.pyversion }} + coverage html -d htmlcov-tests-${{ matrix.pyversion }} --include="tests/*" + diff-cover --compare-branch=origin/master --html-report coverage-diff-${{ matrix.pyversion }}.html coverage.xml - name: Pylint run: | pylint --version pylint --exit-zero xcp/ tests/ setup.py pylint --exit-zero --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" xcp/ tests/ setup.py > pylint.txt - diff-quality --violations=pylint --html-report pylint-diff.html pylint.txt + diff-quality --compare-branch=origin/master --violations=pylint --html-report pylint-diff-${{ matrix.pyversion }}.html pylint.txt - uses: actions/upload-artifact@v3 with: name: Coverage and pylint reports path: | - coverage-diff.html - pylint-diff.html - htmlcov - htmlcov-tests + coverage-diff-${{ matrix.pyversion }}.html + pylint-diff-${{ matrix.pyversion }}.html + htmlcov-${{ matrix.pyversion }} + htmlcov-tests-${{ matrix.pyversion }} diff --git a/requirements-dev.txt b/requirements-dev.txt index 4debd01d..ce760b45 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,3 +12,4 @@ future # python-2.7 only configparser ; python_version < "3.0" +pyliblzma ; python_version < "3.0" From 67c2f6657cee96d75d5d367ea355525c08583c23 Mon Sep 17 00:00:00 2001 From: Bernhard Kaindl Date: Mon, 24 Apr 2023 14:37:50 +0200 Subject: [PATCH 22/24] add tests/branding.py to fix tests/test_bootloader.py Even though .github/workflows/main.yml does a curl of branding.py GitHub CI still failed with ImportError for branding. Signed-off-by: Bernhard Kaindl --- tests/branding.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/branding.py diff --git a/tests/branding.py b/tests/branding.py new file mode 100644 index 00000000..a6d33331 --- /dev/null +++ b/tests/branding.py @@ -0,0 +1,36 @@ +BRAND_CONSOLE_URL = 'https://xcp-ng.org' +BRAND_CONSOLE = 'XCP-ng Center' +BRAND_GUEST_SHORT = 'VM' +BRAND_GUESTS_SHORT = 'VMs' +BRAND_GUESTS = 'Virtual Machines' +BRAND_GUEST = 'Virtual Machine' +BRAND_SERVERS = 'XCP-ng Hosts' +BRAND_SERVER = 'XCP-ng Host' +BRAND_VDI = '' +COMPANY_DOMAIN = 'xcp-ng.org' +COMPANY_NAME_LEGAL = 'Open Source' +COMPANY_NAME = 'Open Source' +COMPANY_NAME_SHORT = 'Open Source' +COMPANY = 'Open Source' +COMPANY_PRODUCT_BRAND = 'XCP-ng' +COMPANY_WEBSITE = 'https://xcp-ng.org' +COPYRIGHT_YEARS = '2018-2022' +ISO_PV_TOOLS_COPYRIGHT = 'XCP-ng' +ISO_PV_TOOLS_LABEL = 'XCP-ng VM Tools' +ISO_PV_TOOLS_PUBLISHER = 'XCP-ng' +PLATFORM_MAJOR_VERSION = '3' +PLATFORM_MICRO_VERSION = '1' +PLATFORM_MINOR_VERSION = '2' +PLATFORM_NAME = 'XCP' +PLATFORM_ORGANISATION = 'xen.org' +PLATFORM_VERSION = '3.2.1' +PLATFORM_WEBSITE = 'www.xen.org' +PRODUCT_BRAND = 'XCP-ng' +PRODUCT_BRAND_DASHED = 'XCP-ng' +PRODUCT_MAJOR_VERSION = '8' +PRODUCT_MICRO_VERSION = '1' +PRODUCT_MINOR_VERSION = '2' +PRODUCT_NAME = 'xenenterprise' +PRODUCT_VERSION = '8.2.1' +PRODUCT_VERSION_TEXT = '8.2' +PRODUCT_VERSION_TEXT_SHORT = '8.2' From 1651397cf43c36d225ca3aaa835004768aaccab7 Mon Sep 17 00:00:00 2001 From: Bernhard Kaindl Date: Mon, 24 Apr 2023 13:03:03 +0200 Subject: [PATCH 23/24] xcp.accessor, xcp.repository: Use binary mode for file I/O Signed-off-by: Bernhard Kaindl --- xcp/accessor.py | 8 ++++---- xcp/repository.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xcp/accessor.py b/xcp/accessor.py index 6d057927..b4e7572b 100644 --- a/xcp/accessor.py +++ b/xcp/accessor.py @@ -101,7 +101,7 @@ def __init__(self, location, ro): def openAddress(self, address): try: - filehandle = open(os.path.join(self.location, address), 'r') + filehandle = open(os.path.join(self.location, address), 'rb') except OSError as e: if e.errno == errno.EIO: self.lastError = 5 @@ -167,7 +167,7 @@ def finish(self): def writeFile(self, in_fh, out_name): logger.info("Copying to %s" % os.path.join(self.location, out_name)) - out_fh = open(os.path.join(self.location, out_name), 'w') + out_fh = open(os.path.join(self.location, out_name), "wb") return self._writeFile(in_fh, out_fh) def __del__(self): @@ -222,7 +222,7 @@ def __init__(self, baseAddress, ro): def openAddress(self, address): try: - file = open(os.path.join(self.baseAddress, address)) + file = open(os.path.join(self.baseAddress, address), "rb") except IOError as e: if e.errno == errno.EIO: self.lastError = 5 @@ -242,7 +242,7 @@ def openAddress(self, address): def writeFile(self, in_fh, out_name): logger.info("Copying to %s" % os.path.join(self.baseAddress, out_name)) - out_fh = open(os.path.join(self.baseAddress, out_name), 'w') + out_fh = open(os.path.join(self.baseAddress, out_name), "wb" ) return self._writeFile(in_fh, out_fh) def __repr__(self): diff --git a/xcp/repository.py b/xcp/repository.py index 35a2c08d..b10aa092 100644 --- a/xcp/repository.py +++ b/xcp/repository.py @@ -297,7 +297,7 @@ def _parse_repofile(self, repofile): repofile.close() # update md5sum for repo - self._md5.update(repofile_contents.encode()) + self._md5.update(repofile_contents) # build xml doc object try: From b5bd2e22374f3d1001f95cdf3cd9a4d40c8d375e Mon Sep 17 00:00:00 2001 From: Bernhard Kaindl Date: Mon, 24 Apr 2023 13:47:46 +0200 Subject: [PATCH 24/24] xcp.accessor: Add the option to pass kwargs like encoding and errors for de/encoding Signed-off-by: Bernhard Kaindl --- xcp/accessor.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/xcp/accessor.py b/xcp/accessor.py index b4e7572b..d8ba26c2 100644 --- a/xcp/accessor.py +++ b/xcp/accessor.py @@ -99,9 +99,10 @@ def __init__(self, location, ro): super(FilesystemAccessor, self).__init__(ro) self.location = location - def openAddress(self, address): + def openAddress(self, address, **kwargs): try: - filehandle = open(os.path.join(self.location, address), 'rb') + kwargs["mode"] = "r" if "encoding" in kwargs else "rb" + filehandle = open(os.path.join(self.location, address), **kwargs) except OSError as e: if e.errno == errno.EIO: self.lastError = 5 @@ -165,9 +166,10 @@ def finish(self): os.rmdir(self.location) self.location = None - def writeFile(self, in_fh, out_name): + def writeFile(self, in_fh, out_name, **kwargs): logger.info("Copying to %s" % os.path.join(self.location, out_name)) - out_fh = open(os.path.join(self.location, out_name), "wb") + kwargs["mode"] = "w" if "encoding" in kwargs else "wb" + out_fh = open(os.path.join(self.location, out_name), **kwargs) return self._writeFile(in_fh, out_fh) def __del__(self): @@ -220,9 +222,10 @@ def __init__(self, baseAddress, ro): super(FileAccessor, self).__init__(ro) self.baseAddress = baseAddress - def openAddress(self, address): + def openAddress(self, address, **kwargs): try: - file = open(os.path.join(self.baseAddress, address), "rb") + kwargs["mode"] = "r" if "encoding" in kwargs else "rb" + file = open(os.path.join(self.baseAddress, address), **kwargs) except IOError as e: if e.errno == errno.EIO: self.lastError = 5 @@ -240,9 +243,10 @@ def openAddress(self, address): return False return file - def writeFile(self, in_fh, out_name): + def writeFile(self, in_fh, out_name, **kwargs): logger.info("Copying to %s" % os.path.join(self.baseAddress, out_name)) - out_fh = open(os.path.join(self.baseAddress, out_name), "wb" ) + kwargs["mode"] = "w" if "encoding" in kwargs else "wb" + out_fh = open(os.path.join(self.baseAddress, out_name), **kwargs) return self._writeFile(in_fh, out_fh) def __repr__(self): @@ -331,13 +335,14 @@ def access(self, path): self.lastError = 500 return False - def openAddress(self, address): + def openAddress(self, address, **kwargs): logger.debug("Opening "+address) self._cleanup() url = urllib.parse.unquote(address) self.ftp.voidcmd('TYPE I') - s = self.ftp.transfercmd('RETR ' + url).makefile('rb') + kwargs["mode"] = "r" if "encoding" in kwargs else "rb" + s = self.ftp.transfercmd('RETR ' + url).makefile(**kwargs) self.cleanup = True return s