]>
Commit | Line | Data |
---|---|---|
187be27c JS |
1 | """ |
2 | QEMU Object Model FUSE filesystem tool | |
3 | ||
4 | This script offers a simple FUSE filesystem within which the QOM tree | |
5 | may be browsed, queried and edited using traditional shell tooling. | |
6 | ||
7 | This script requires the 'fusepy' python package. | |
8 | ||
187be27c | 9 | |
2aa10179 JS |
10 | usage: qom-fuse [-h] [--socket SOCKET] <mount> |
11 | ||
12 | Mount a QOM tree as a FUSE filesystem | |
13 | ||
14 | positional arguments: | |
15 | <mount> Mount point | |
16 | ||
17 | optional arguments: | |
18 | -h, --help show this help message and exit | |
19 | --socket SOCKET, -s SOCKET | |
20 | QMP socket path or address (addr:port). May also be | |
21 | set via QMP_SOCKET environment variable. | |
187be27c | 22 | """ |
5ade7674 | 23 | ## |
5ade7674 | 24 | # Copyright IBM, Corp. 2012 |
f713ed4f | 25 | # Copyright (C) 2020 Red Hat, Inc. |
5ade7674 AL |
26 | # |
27 | # Authors: | |
28 | # Anthony Liguori <aliguori@us.ibm.com> | |
f713ed4f | 29 | # Markus Armbruster <armbru@redhat.com> |
5ade7674 | 30 | # |
26c1ccad JS |
31 | # This work is licensed under the terms of the GNU GPL, version 2 or later. |
32 | # See the COPYING file in the top-level directory. | |
5ade7674 AL |
33 | ## |
34 | ||
2aa10179 | 35 | import argparse |
26c1ccad | 36 | from errno import ENOENT, EPERM |
c6b7eae9 JS |
37 | import stat |
38 | import sys | |
30ec845c JS |
39 | from typing import ( |
40 | IO, | |
41 | Dict, | |
42 | Iterator, | |
43 | Mapping, | |
44 | Optional, | |
45 | Union, | |
46 | ) | |
c6b7eae9 JS |
47 | |
48 | import fuse | |
49 | from fuse import FUSE, FuseOSError, Operations | |
50 | ||
37094b6d | 51 | from qemu.qmp import ExecuteError |
8d6cdc51 | 52 | |
173d185d | 53 | from .qom_common import QOMCommand |
5ade7674 | 54 | |
c6b7eae9 | 55 | |
5ade7674 AL |
56 | fuse.fuse_python_api = (0, 2) |
57 | ||
26c1ccad | 58 | |
2aa10179 JS |
59 | class QOMFuse(QOMCommand, Operations): |
60 | """ | |
61 | QOMFuse implements both fuse.Operations and QOMCommand. | |
62 | ||
63 | Operations implements the FS, and QOMCommand implements the CLI command. | |
64 | """ | |
65 | name = 'fuse' | |
66 | help = 'Mount a QOM tree as a FUSE filesystem' | |
67 | fuse: FUSE | |
68 | ||
69 | @classmethod | |
70 | def configure_parser(cls, parser: argparse.ArgumentParser) -> None: | |
71 | super().configure_parser(parser) | |
72 | parser.add_argument( | |
73 | 'mount', | |
74 | metavar='<mount>', | |
75 | action='store', | |
76 | help="Mount point", | |
77 | ) | |
78 | ||
79 | def __init__(self, args: argparse.Namespace): | |
80 | super().__init__(args) | |
81 | self.mount = args.mount | |
82 | self.ino_map: Dict[str, int] = {} | |
5ade7674 AL |
83 | self.ino_count = 1 |
84 | ||
2aa10179 JS |
85 | def run(self) -> int: |
86 | print(f"Mounting QOMFS to '{self.mount}'", file=sys.stderr) | |
87 | self.fuse = FUSE(self, self.mount, foreground=True) | |
88 | return 0 | |
89 | ||
30ec845c | 90 | def get_ino(self, path: str) -> int: |
187be27c | 91 | """Get an inode number for a given QOM path.""" |
d7a4228e | 92 | if path in self.ino_map: |
5ade7674 AL |
93 | return self.ino_map[path] |
94 | self.ino_map[path] = self.ino_count | |
95 | self.ino_count += 1 | |
96 | return self.ino_map[path] | |
97 | ||
30ec845c | 98 | def is_object(self, path: str) -> bool: |
187be27c | 99 | """Is the given QOM path an object?""" |
5ade7674 | 100 | try: |
9ec8a386 | 101 | self.qom_list(path) |
5ade7674 | 102 | return True |
8d6cdc51 | 103 | except ExecuteError: |
5ade7674 AL |
104 | return False |
105 | ||
30ec845c | 106 | def is_property(self, path: str) -> bool: |
187be27c | 107 | """Is the given QOM path a property?""" |
3a14019e MA |
108 | path, prop = path.rsplit('/', 1) |
109 | if path == '': | |
110 | path = '/' | |
5ade7674 | 111 | try: |
9ec8a386 JS |
112 | for item in self.qom_list(path): |
113 | if item.name == prop: | |
5ade7674 AL |
114 | return True |
115 | return False | |
8d6cdc51 | 116 | except ExecuteError: |
5ade7674 AL |
117 | return False |
118 | ||
30ec845c | 119 | def is_link(self, path: str) -> bool: |
187be27c | 120 | """Is the given QOM path a link?""" |
3a14019e MA |
121 | path, prop = path.rsplit('/', 1) |
122 | if path == '': | |
123 | path = '/' | |
5ade7674 | 124 | try: |
9ec8a386 JS |
125 | for item in self.qom_list(path): |
126 | if item.name == prop and item.link: | |
127 | return True | |
5ade7674 | 128 | return False |
8d6cdc51 | 129 | except ExecuteError: |
5ade7674 AL |
130 | return False |
131 | ||
30ec845c | 132 | def read(self, path: str, size: int, offset: int, fh: IO[bytes]) -> bytes: |
5ade7674 | 133 | if not self.is_property(path): |
2cea7134 | 134 | raise FuseOSError(ENOENT) |
5ade7674 AL |
135 | |
136 | path, prop = path.rsplit('/', 1) | |
3a14019e MA |
137 | if path == '': |
138 | path = '/' | |
5ade7674 | 139 | try: |
684750ab | 140 | data = str(self.qmp.cmd('qom-get', path=path, property=prop)) |
26c1ccad | 141 | data += '\n' # make values shell friendly |
8d6cdc51 | 142 | except ExecuteError as err: |
7552823a | 143 | raise FuseOSError(EPERM) from err |
5ade7674 AL |
144 | |
145 | if offset > len(data): | |
2cea7134 | 146 | return b'' |
5ade7674 | 147 | |
7552823a | 148 | return bytes(data[offset:][:size], encoding='utf-8') |
5ade7674 | 149 | |
30ec845c | 150 | def readlink(self, path: str) -> Union[bool, str]: |
5ade7674 AL |
151 | if not self.is_link(path): |
152 | return False | |
153 | path, prop = path.rsplit('/', 1) | |
154 | prefix = '/'.join(['..'] * (len(path.split('/')) - 1)) | |
684750ab VSO |
155 | return prefix + str(self.qmp.cmd('qom-get', path=path, |
156 | property=prop)) | |
5ade7674 | 157 | |
30ec845c JS |
158 | def getattr(self, path: str, |
159 | fh: Optional[IO[bytes]] = None) -> Mapping[str, object]: | |
5ade7674 | 160 | if self.is_link(path): |
26c1ccad JS |
161 | value = { |
162 | 'st_mode': 0o755 | stat.S_IFLNK, | |
163 | 'st_ino': self.get_ino(path), | |
164 | 'st_dev': 0, | |
165 | 'st_nlink': 2, | |
166 | 'st_uid': 1000, | |
167 | 'st_gid': 1000, | |
168 | 'st_size': 4096, | |
169 | 'st_atime': 0, | |
170 | 'st_mtime': 0, | |
171 | 'st_ctime': 0 | |
172 | } | |
5ade7674 | 173 | elif self.is_object(path): |
26c1ccad JS |
174 | value = { |
175 | 'st_mode': 0o755 | stat.S_IFDIR, | |
176 | 'st_ino': self.get_ino(path), | |
177 | 'st_dev': 0, | |
178 | 'st_nlink': 2, | |
179 | 'st_uid': 1000, | |
180 | 'st_gid': 1000, | |
181 | 'st_size': 4096, | |
182 | 'st_atime': 0, | |
183 | 'st_mtime': 0, | |
184 | 'st_ctime': 0 | |
185 | } | |
5ade7674 | 186 | elif self.is_property(path): |
26c1ccad JS |
187 | value = { |
188 | 'st_mode': 0o644 | stat.S_IFREG, | |
189 | 'st_ino': self.get_ino(path), | |
190 | 'st_dev': 0, | |
191 | 'st_nlink': 1, | |
192 | 'st_uid': 1000, | |
193 | 'st_gid': 1000, | |
194 | 'st_size': 4096, | |
195 | 'st_atime': 0, | |
196 | 'st_mtime': 0, | |
197 | 'st_ctime': 0 | |
198 | } | |
5ade7674 | 199 | else: |
f713ed4f | 200 | raise FuseOSError(ENOENT) |
5ade7674 AL |
201 | return value |
202 | ||
30ec845c | 203 | def readdir(self, path: str, fh: IO[bytes]) -> Iterator[str]: |
f713ed4f MA |
204 | yield '.' |
205 | yield '..' | |
9ec8a386 JS |
206 | for item in self.qom_list(path): |
207 | yield item.name |