| 1 | #!/usr/bin/env python3 |
| 2 | # group: img |
| 3 | # |
| 4 | # Test case for NBD's blockdev-add interface |
| 5 | # |
| 6 | # Copyright (C) 2016 Red Hat, Inc. |
| 7 | # |
| 8 | # This program is free software; you can redistribute it and/or modify |
| 9 | # it under the terms of the GNU General Public License as published by |
| 10 | # the Free Software Foundation; either version 2 of the License, or |
| 11 | # (at your option) any later version. |
| 12 | # |
| 13 | # This program is distributed in the hope that it will be useful, |
| 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | # GNU General Public License for more details. |
| 17 | # |
| 18 | # You should have received a copy of the GNU General Public License |
| 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 20 | # |
| 21 | |
| 22 | import os |
| 23 | import random |
| 24 | import socket |
| 25 | import stat |
| 26 | import time |
| 27 | import iotests |
| 28 | from iotests import cachemode, aiomode, imgfmt, qemu_img, qemu_nbd, qemu_nbd_early_pipe |
| 29 | |
| 30 | NBD_PORT_START = 32768 |
| 31 | NBD_PORT_END = NBD_PORT_START + 1024 |
| 32 | NBD_IPV6_PORT_START = NBD_PORT_END |
| 33 | NBD_IPV6_PORT_END = NBD_IPV6_PORT_START + 1024 |
| 34 | |
| 35 | test_img = os.path.join(iotests.test_dir, 'test.img') |
| 36 | unix_socket = os.path.join(iotests.sock_dir, 'nbd.socket') |
| 37 | |
| 38 | |
| 39 | def flatten_sock_addr(crumpled_address): |
| 40 | result = { 'type': crumpled_address['type'] } |
| 41 | result.update(crumpled_address['data']) |
| 42 | return result |
| 43 | |
| 44 | |
| 45 | class NBDBlockdevAddBase(iotests.QMPTestCase): |
| 46 | def blockdev_add_options(self, address, export, node_name): |
| 47 | options = { 'node-name': node_name, |
| 48 | 'driver': 'raw', |
| 49 | 'file': { |
| 50 | 'driver': 'nbd', |
| 51 | 'read-only': True, |
| 52 | 'server': address |
| 53 | } } |
| 54 | if export is not None: |
| 55 | options['file']['export'] = export |
| 56 | return options |
| 57 | |
| 58 | def client_test(self, filename, address, export=None, |
| 59 | node_name='nbd-blockdev', delete=True): |
| 60 | bao = self.blockdev_add_options(address, export, node_name) |
| 61 | self.vm.cmd('blockdev-add', bao) |
| 62 | |
| 63 | found = False |
| 64 | result = self.vm.qmp('query-named-block-nodes') |
| 65 | for node in result['return']: |
| 66 | if node['node-name'] == node_name: |
| 67 | found = True |
| 68 | if isinstance(filename, str): |
| 69 | self.assert_qmp(node, 'image/filename', filename) |
| 70 | else: |
| 71 | self.assert_json_filename_equal(node['image']['filename'], |
| 72 | filename) |
| 73 | break |
| 74 | self.assertTrue(found) |
| 75 | |
| 76 | if delete: |
| 77 | self.vm.cmd('blockdev-del', node_name=node_name) |
| 78 | |
| 79 | |
| 80 | class QemuNBD(NBDBlockdevAddBase): |
| 81 | def setUp(self): |
| 82 | qemu_img('create', '-f', iotests.imgfmt, test_img, '64k') |
| 83 | self.vm = iotests.VM() |
| 84 | self.vm.launch() |
| 85 | |
| 86 | def tearDown(self): |
| 87 | self.vm.shutdown() |
| 88 | os.remove(test_img) |
| 89 | try: |
| 90 | os.remove(unix_socket) |
| 91 | except OSError: |
| 92 | pass |
| 93 | |
| 94 | def _try_server_up(self, *args): |
| 95 | status, msg = qemu_nbd_early_pipe('-f', imgfmt, test_img, *args) |
| 96 | if status == 0: |
| 97 | return True |
| 98 | if 'Address already in use' in msg: |
| 99 | return False |
| 100 | self.fail(msg) |
| 101 | |
| 102 | def _server_up(self, *args): |
| 103 | self.assertTrue(self._try_server_up(*args)) |
| 104 | |
| 105 | def test_inet(self): |
| 106 | while True: |
| 107 | nbd_port = random.randrange(NBD_PORT_START, NBD_PORT_END) |
| 108 | if self._try_server_up('-b', 'localhost', '-p', str(nbd_port)): |
| 109 | break |
| 110 | |
| 111 | address = { 'type': 'inet', |
| 112 | 'data': { |
| 113 | 'host': 'localhost', |
| 114 | 'port': str(nbd_port) |
| 115 | } } |
| 116 | self.client_test('nbd://localhost:%i' % nbd_port, |
| 117 | flatten_sock_addr(address)) |
| 118 | |
| 119 | def test_unix(self): |
| 120 | self._server_up('-k', unix_socket) |
| 121 | address = { 'type': 'unix', |
| 122 | 'data': { 'path': unix_socket } } |
| 123 | self.client_test('nbd+unix://?socket=' + unix_socket, |
| 124 | flatten_sock_addr(address)) |
| 125 | |
| 126 | |
| 127 | class BuiltinNBD(NBDBlockdevAddBase): |
| 128 | def setUp(self): |
| 129 | qemu_img('create', '-f', iotests.imgfmt, test_img, '64k') |
| 130 | self.vm = iotests.VM() |
| 131 | self.vm.launch() |
| 132 | self.server = iotests.VM('.server') |
| 133 | self.server.add_drive_raw('if=none,id=nbd-export,' + |
| 134 | 'file=%s,' % test_img + |
| 135 | 'format=%s,' % imgfmt + |
| 136 | 'cache=%s,' % cachemode + |
| 137 | 'aio=%s' % aiomode) |
| 138 | self.server.launch() |
| 139 | |
| 140 | def tearDown(self): |
| 141 | self.vm.shutdown() |
| 142 | self.server.shutdown() |
| 143 | os.remove(test_img) |
| 144 | try: |
| 145 | os.remove(unix_socket) |
| 146 | except OSError: |
| 147 | pass |
| 148 | |
| 149 | # Returns False on EADDRINUSE; fails an assertion on other errors. |
| 150 | # Returns True on success. |
| 151 | def _try_server_up(self, address, export_name=None, export_name2=None): |
| 152 | result = self.server.qmp('nbd-server-start', addr=address) |
| 153 | if 'error' in result and \ |
| 154 | 'Address already in use' in result['error']['desc']: |
| 155 | return False |
| 156 | self.assert_qmp(result, 'return', {}) |
| 157 | |
| 158 | if export_name is None: |
| 159 | self.server.cmd('nbd-server-add', device='nbd-export') |
| 160 | else: |
| 161 | self.server.cmd('nbd-server-add', device='nbd-export', |
| 162 | name=export_name) |
| 163 | |
| 164 | if export_name2 is not None: |
| 165 | self.server.cmd('nbd-server-add', device='nbd-export', |
| 166 | name=export_name2) |
| 167 | |
| 168 | return True |
| 169 | |
| 170 | def _server_up(self, address, export_name=None, export_name2=None): |
| 171 | self.assertTrue(self._try_server_up(address, export_name, export_name2)) |
| 172 | |
| 173 | def _server_down(self): |
| 174 | self.server.cmd('nbd-server-stop') |
| 175 | |
| 176 | def do_test_inet(self, export_name=None): |
| 177 | while True: |
| 178 | nbd_port = random.randrange(NBD_PORT_START, NBD_PORT_END) |
| 179 | address = { 'type': 'inet', |
| 180 | 'data': { |
| 181 | 'host': 'localhost', |
| 182 | 'port': str(nbd_port) |
| 183 | } } |
| 184 | if self._try_server_up(address, export_name): |
| 185 | break |
| 186 | |
| 187 | export_name = export_name or 'nbd-export' |
| 188 | self.client_test('nbd://localhost:%i/%s' % (nbd_port, export_name), |
| 189 | flatten_sock_addr(address), export_name) |
| 190 | self._server_down() |
| 191 | |
| 192 | def test_inet_default_export_name(self): |
| 193 | self.do_test_inet() |
| 194 | |
| 195 | def test_inet_same_export_name(self): |
| 196 | self.do_test_inet('nbd-export') |
| 197 | |
| 198 | def test_inet_different_export_name(self): |
| 199 | self.do_test_inet('shadow') |
| 200 | |
| 201 | def test_inet_two_exports(self): |
| 202 | while True: |
| 203 | nbd_port = random.randrange(NBD_PORT_START, NBD_PORT_END) |
| 204 | address = { 'type': 'inet', |
| 205 | 'data': { |
| 206 | 'host': 'localhost', |
| 207 | 'port': str(nbd_port) |
| 208 | } } |
| 209 | if self._try_server_up(address, 'exp1', 'exp2'): |
| 210 | break |
| 211 | |
| 212 | self.client_test('nbd://localhost:%i/%s' % (nbd_port, 'exp1'), |
| 213 | flatten_sock_addr(address), 'exp1', 'node1', False) |
| 214 | self.client_test('nbd://localhost:%i/%s' % (nbd_port, 'exp2'), |
| 215 | flatten_sock_addr(address), 'exp2', 'node2', False) |
| 216 | self.vm.cmd('blockdev-del', node_name='node1') |
| 217 | self.vm.cmd('blockdev-del', node_name='node2') |
| 218 | self._server_down() |
| 219 | |
| 220 | def test_inet6(self): |
| 221 | try: |
| 222 | socket.getaddrinfo("::0", "0", socket.AF_INET6, |
| 223 | socket.SOCK_STREAM, socket.IPPROTO_TCP, |
| 224 | socket.AI_ADDRCONFIG | socket.AI_CANONNAME) |
| 225 | except socket.gaierror: |
| 226 | # IPv6 not available, skip |
| 227 | return |
| 228 | |
| 229 | while True: |
| 230 | nbd_port = random.randrange(NBD_IPV6_PORT_START, NBD_IPV6_PORT_END) |
| 231 | address = { 'type': 'inet', |
| 232 | 'data': { |
| 233 | 'host': '::1', |
| 234 | 'port': str(nbd_port), |
| 235 | 'ipv4': False, |
| 236 | 'ipv6': True |
| 237 | } } |
| 238 | if self._try_server_up(address): |
| 239 | break |
| 240 | |
| 241 | filename = { 'driver': 'raw', |
| 242 | 'file': { |
| 243 | 'driver': 'nbd', |
| 244 | 'export': 'nbd-export', |
| 245 | 'server': flatten_sock_addr(address) |
| 246 | } } |
| 247 | self.client_test(filename, flatten_sock_addr(address), 'nbd-export') |
| 248 | self._server_down() |
| 249 | |
| 250 | def test_unix(self): |
| 251 | address = { 'type': 'unix', |
| 252 | 'data': { 'path': unix_socket } } |
| 253 | self._server_up(address) |
| 254 | self.client_test('nbd+unix:///nbd-export?socket=' + unix_socket, |
| 255 | flatten_sock_addr(address), 'nbd-export') |
| 256 | self._server_down() |
| 257 | |
| 258 | def test_fd(self): |
| 259 | self._server_up({ 'type': 'unix', |
| 260 | 'data': { 'path': unix_socket } }) |
| 261 | |
| 262 | sockfd = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
| 263 | sockfd.connect(unix_socket) |
| 264 | |
| 265 | result = self.vm.send_fd_scm(fd=sockfd.fileno()) |
| 266 | self.assertEqual(result, 0, 'Failed to send socket FD') |
| 267 | |
| 268 | self.vm.cmd('getfd', fdname='nbd-fifo') |
| 269 | |
| 270 | address = { 'type': 'fd', |
| 271 | 'data': { 'str': 'nbd-fifo' } } |
| 272 | filename = { 'driver': 'raw', |
| 273 | 'file': { |
| 274 | 'driver': 'nbd', |
| 275 | 'export': 'nbd-export', |
| 276 | 'server': flatten_sock_addr(address) |
| 277 | } } |
| 278 | self.client_test(filename, flatten_sock_addr(address), 'nbd-export') |
| 279 | |
| 280 | sockfd.close() |
| 281 | self._server_down() |
| 282 | |
| 283 | |
| 284 | if __name__ == '__main__': |
| 285 | iotests.main(supported_fmts=['raw'], |
| 286 | supported_protocols=['nbd']) |