Skip to content

Commit b9a8135

Browse files
authored
Merge pull request #350 from xcp-ng/dnt/fast-xva-bridge
Add fast xva_bridge.py script
2 parents 14c5d4b + bb42b71 commit b9a8135

File tree

7 files changed

+170
-144
lines changed

7 files changed

+170
-144
lines changed

README.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -528,21 +528,26 @@ PXE_CONFIG_SERVER = 'pxe'
528528

529529
The `installer` parameter is optional. If you leave it empty it will be automatically defined as `http://<PXE_CONFIG_SERVER>/installers/xcp-ng/<version>/`.
530530

531-
## Bash scripts
531+
## xva_bridge.py
532532

533-
* get_xva_bridge.sh: a script to get the XAPI bridge value from inside a xva file and the compression method used for this xva file.
533+
This script gets and sets the XAPI bridge and compression method of an XVA file. It requires libarchive-c==5.3.
534+
535+
To print an XVA file's bridge value and compression method:
534536

535537
```
536-
$ /path/to/get_xva_bridge.sh alpine-minimal-3.12.0.xva
537-
ova.xml
538-
alpine-minimal-3.12.0.xva's bridge network is: xapi1 and its compression method is: tar.
538+
$ xva_bridge.py alpine-minimal-3.12.0.xva -v
539+
DEBUG:root:Compression: zstd
540+
DEBUG:root:Header is 23889 bytes
541+
INFO:root:Found bridge xenbr0
539542
```
540543

541-
* set_xva_bridge.sh: a script to modify the XAPI bridge value inside a xva file and the compression method used for this xva file if wanted. The original xva file is saved before modification.
544+
To set an XVA file's bridge value and compression method. By default, the script will save the resulting archive to `xva_path.new`:
542545

543546
```
544-
- Usage: /path/to/set_xva_bridge.sh [XVA_filename] compression[zstd|gzip] bridge_value[xenbr0|xapi[:9]|...]
545-
- All options are mandatory.
546-
547-
$ /path/to/set_xva_bridge.sh alpine-minimal-3.12.0.xva zstd xenbr0
547+
$ xva_bridge.py alpine-minimal-3.12.0.xva --set-bridge xenbr0
548+
INFO:root:Found bridge xapi1
549+
INFO:root:Output path: alpine-minimal-3.12.0.xva.new
550+
INFO:root:Setting bridge to xenbr0
548551
```
552+
553+
For more details, see `xva_bridge.py --help`.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dev = [
2828
"ruff",
2929
"types-requests",
3030
"typing-extensions",
31+
"libarchive-c==5.3",
3132
]
3233

3334
[tool.pyright]

requirements/dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ pyyaml>=6.0
1010
ruff
1111
types-requests
1212
typing-extensions
13+
libarchive-c==5.3
1314
-r base.txt

scripts/get_xva_bridge.sh

Lines changed: 0 additions & 36 deletions
This file was deleted.

scripts/set_xva_bridge.sh

Lines changed: 0 additions & 97 deletions
This file was deleted.

scripts/xva_bridge.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env python3
2+
3+
# Tested on libarchive-c==5.3. Due to our use of library internals, may not work on other versions of libarchive-c.
4+
5+
import argparse
6+
import io
7+
import logging
8+
import os
9+
from xml.dom import minidom
10+
11+
import libarchive
12+
import libarchive.ffi
13+
14+
class XvaHeaderMember:
15+
def __init__(self, member: minidom.Element):
16+
self.member = member
17+
18+
def get_name(self):
19+
for child in self.member.childNodes:
20+
if child.nodeType == minidom.Node.ELEMENT_NODE and child.tagName == "name" and child.firstChild:
21+
return child.firstChild.nodeValue
22+
return None
23+
24+
def get_value(self):
25+
for child in self.member.childNodes:
26+
if child.nodeType == minidom.Node.ELEMENT_NODE and child.tagName == "value" and child.firstChild:
27+
return child.firstChild.nodeValue
28+
return None
29+
30+
def set_value(self, value: str):
31+
for child in self.member.childNodes:
32+
if child.nodeType == minidom.Node.ELEMENT_NODE and child.tagName == "value" and child.firstChild:
33+
child.firstChild.nodeValue = value # type: ignore
34+
return None
35+
36+
37+
class XvaHeader:
38+
def __init__(self, header_bytes: bytes):
39+
self.xml = minidom.parseString(header_bytes.decode())
40+
41+
def members(self):
42+
for member in self.xml.getElementsByTagName("member"):
43+
if member.nodeType == minidom.Node.ELEMENT_NODE:
44+
yield XvaHeaderMember(member)
45+
46+
def get_bridge(self):
47+
for member in self.members():
48+
if member.get_name() == "bridge":
49+
return member.get_value()
50+
raise ValueError("Could not find bridge value in XVA header")
51+
52+
def set_bridge(self, bridge: str):
53+
for member in self.members():
54+
if member.get_name() == "bridge":
55+
member.set_value(bridge)
56+
return
57+
raise ValueError("Could not find bridge value in XVA header")
58+
59+
60+
if __name__ == "__main__":
61+
parser = argparse.ArgumentParser()
62+
parser.add_argument("xva", help="input file path")
63+
parser.add_argument(
64+
"--set-bridge", help="new bridge value of format `xenbr0|xapi[:9]|...`; omit this option to show current bridge"
65+
)
66+
parser.add_argument(
67+
"--compression",
68+
choices=["zstd", "gzip"],
69+
default="zstd",
70+
help="compression mode of new XVA when setting bridge value (default: zstd)",
71+
)
72+
parser.add_argument("-o", "--output", help="output file path (must not be the same as input)")
73+
parser.add_argument("--backup-path", help="backup file path")
74+
parser.add_argument(
75+
"--in-place", action="store_true", help="rename output file to input file; rename input file to backup file"
76+
)
77+
parser.add_argument("-v", "--verbose", action="store_true", help="verbose logging")
78+
args = parser.parse_args()
79+
80+
if args.verbose:
81+
logging.getLogger().setLevel(logging.DEBUG)
82+
else:
83+
logging.getLogger().setLevel(logging.INFO)
84+
85+
with libarchive.file_reader(args.xva, "tar") as input_file:
86+
logging.debug(f"Compression: {', '.join(filter.decode() for filter in input_file.filter_names)}")
87+
88+
entry_iter = iter(input_file)
89+
90+
header_entry = next(entry_iter)
91+
if header_entry.pathname != "ova.xml":
92+
raise ValueError("Unexpected header entry name")
93+
with io.BytesIO() as header_writer:
94+
for block in header_entry.get_blocks():
95+
header_writer.write(block)
96+
header_bytes = header_writer.getvalue()
97+
98+
logging.debug(f"Header is {len(header_bytes)} bytes")
99+
100+
header = XvaHeader(header_bytes)
101+
bridge = header.get_bridge()
102+
logging.info(f"Found bridge {bridge}")
103+
104+
if args.set_bridge:
105+
output_path = args.output
106+
if not output_path:
107+
output_path = args.xva + ".new"
108+
logging.info(f"Output path: {output_path}")
109+
110+
logging.info(f"Setting bridge to {args.set_bridge}")
111+
header.set_bridge(args.set_bridge)
112+
113+
logging.debug(f"Using compression {args.compression}")
114+
with libarchive.file_writer(output_path, "pax_restricted", args.compression) as output_file:
115+
new_header_bytes = header.xml.toxml().encode()
116+
output_file.add_file_from_memory(
117+
"ova.xml", len(new_header_bytes), new_header_bytes, permission=0o400, uid=0, gid=0
118+
)
119+
120+
for entry in entry_iter:
121+
logging.debug(f"Copying {entry.pathname}: {entry.size} bytes")
122+
new_entry = libarchive.ArchiveEntry(entry.header_codec, perm=0o400, uid=0, gid=0)
123+
for attr in ["filetype", "pathname", "size"]:
124+
setattr(new_entry, attr, getattr(entry, attr))
125+
126+
# ArchiveEntry doesn't expose block copying, so write the entry manually via the FFI interface
127+
libarchive.ffi.write_header(output_file._pointer, new_entry._entry_p)
128+
for block in entry.get_blocks():
129+
libarchive.ffi.write_data(output_file._pointer, block, len(block))
130+
libarchive.ffi.write_finish_entry(output_file._pointer)
131+
132+
if args.in_place:
133+
backup_path = args.backup_path
134+
if not backup_path:
135+
backup_path = args.xva + ".bak"
136+
logging.info(f"Backup path: {backup_path}")
137+
138+
logging.info(f"Renaming {args.xva} -> {backup_path}")
139+
os.rename(args.xva, backup_path)
140+
logging.info(f"Renaming {output_path} -> {args.xva}")
141+
os.rename(output_path, args.xva)

uv.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)