Skip to content

Commit 4292c9b

Browse files
committed
Add fast xva_bridge.py script
Signed-off-by: Tu Dinh <[email protected]>
1 parent 2f30ecc commit 4292c9b

File tree

4 files changed

+142
-143
lines changed

4 files changed

+142
-143
lines changed

README.md

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

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

515-
## Bash scripts
515+
## xva_bridge.py
516516

517-
* 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.
517+
This script gets and sets the XAPI bridge and compression method of an XVA file. It requires libarchive-c==5.3.
518+
519+
To print an XVA file's bridge value and compression method:
518520

519521
```
520-
$ /path/to/get_xva_bridge.sh alpine-minimal-3.12.0.xva
521-
ova.xml
522-
alpine-minimal-3.12.0.xva's bridge network is: xapi1 and its compression method is: tar.
522+
$ xva_bridge.py alpine-minimal-3.12.0.xva -v
523+
DEBUG:root:Compression: zstd
524+
DEBUG:root:Header is 23889 bytes
525+
INFO:root:Found bridge xenbr0
523526
```
524527

525-
* 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.
528+
To set an XVA file's bridge value and compression method. By default, the script will save the resulting archive to `xva_path.new`:
526529

527530
```
528-
- Usage: /path/to/set_xva_bridge.sh [XVA_filename] compression[zstd|gzip] bridge_value[xenbr0|xapi[:9]|...]
529-
- All options are mandatory.
530-
531-
$ /path/to/set_xva_bridge.sh alpine-minimal-3.12.0.xva zstd xenbr0
531+
$ xva_bridge.py alpine-minimal-3.12.0.xva --set-bridge xenbr0
532+
INFO:root:Found bridge xapi1
533+
INFO:root:Output path: alpine-minimal-3.12.0.xva.new
534+
INFO:root:Setting bridge to xenbr0
532535
```
536+
537+
For more details, see `xva_bridge.py --help`.

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

0 commit comments

Comments
 (0)