/* * QEMU Block driver for NBD * * Copyright (c) 2021 Virtuozzo International GmbH. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "qemu/osdep.h" #include "block/nbd.h" #include "qapi/qapi-visit-sockets.h" #include "qapi/clone-visitor.h" struct NBDClientConnection { /* Initialization constants, never change */ SocketAddress *saddr; /* address to connect to */ QCryptoTLSCreds *tlscreds; char *tlshostname; NBDExportInfo initial_info; bool do_negotiation; bool do_retry; QemuMutex mutex; NBDExportInfo updated_info; /* * @sioc represents a successful result. While thread is running, @sioc is * used only by thread and not protected by mutex. When thread is not * running, @sioc is stolen by nbd_co_establish_connection() under mutex. */ QIOChannelSocket *sioc; QIOChannel *ioc; /* * @err represents previous attempt. It may be copied by * nbd_co_establish_connection() when it reports failure. */ Error *err; /* All further fields are accessed only under mutex */ bool running; /* thread is running now */ bool detached; /* thread is detached and should cleanup the state */ /* * wait_co: if non-NULL, which coroutine to wake in * nbd_co_establish_connection() after yield() */ Coroutine *wait_co; }; /* * The function isn't protected by any mutex, only call it when the client * connection attempt has not yet started. */ void nbd_client_connection_enable_retry(NBDClientConnection *conn) { conn->do_retry = true; } NBDClientConnection *nbd_client_connection_new(const SocketAddress *saddr, bool do_negotiation, const char *export_name, const char *x_dirty_bitmap, QCryptoTLSCreds *tlscreds, const char *tlshostname) { NBDClientConnection *conn = g_new(NBDClientConnection, 1); object_ref(OBJECT(tlscreds)); *conn = (NBDClientConnection) { .saddr = QAPI_CLONE(SocketAddress, saddr), .tlscreds = tlscreds, .tlshostname = g_strdup(tlshostname), .do_negotiation = do_negotiation, .initial_info.request_sizes = true, .initial_info.structured_reply = true, .initial_info.base_allocation = true, .initial_info.x_dirty_bitmap = g_strdup(x_dirty_bitmap), .initial_info.name = g_strdup(export_name ?: "") }; qemu_mutex_init(&conn->mutex); return conn; } static void nbd_client_connection_do_free(NBDClientConnection *conn) { if (conn->sioc) { qio_channel_close(QIO_CHANNEL(conn->sioc), NULL); object_unref(OBJECT(conn->sioc)); } error_free(conn->err); qapi_free_SocketAddress(conn->saddr); g_free(conn->tlshostname); object_unref(OBJECT(conn->tlscreds)); g_free(conn->initial_info.x_dirty_bitmap); g_free(conn->initial_info.name); g_free(conn); } /* * Connect to @addr and do NBD negotiation if @info is not null. If @tlscreds * are given @outioc is returned. @outioc is provided only on success. The call * may be cancelled from other thread by simply qio_channel_shutdown(sioc). */ static int nbd_connect(QIOChannelSocket *sioc, SocketAddress *addr, NBDExportInfo *info, QCryptoTLSCreds *tlscreds, const char *tlshostname, QIOChannel **outioc, Error **errp) { int ret; if (outioc) { *outioc = NULL; } ret = qio_channel_socket_connect_sync(sioc, addr, errp); if (ret < 0) { return ret; } qio_channel_set_delay(QIO_CHANNEL(sioc), false); if (!info) { return 0; } ret = nbd_receive_negotiate(NULL, QIO_CHANNEL(sioc), tlscreds, tlshostname, outioc, info, errp); if (ret < 0) { /* * nbd_receive_negotiate() may setup tls ioc and return it even on * failure path. In this case we should use it instead of original * channel. */ if (outioc && *outioc) { qio_channel_close(QIO_CHANNEL(*outioc), NULL); object_unref(OBJECT(*outioc)); *outioc = NULL; } else { qio_channel_close(QIO_CHANNEL(sioc), NULL); } return ret; } return 0; } static void *connect_thread_func(void *opaque) { NBDClientConnection *conn = opaque; int ret; bool do_free; uint64_t timeout = 1; uint64_t max_timeout = 16; qemu_mutex_lock(&conn->mutex); while (!conn->detached) { Error *local_err = NULL; assert(!conn->sioc); conn->sioc = qio_channel_socket_new(); qemu_mutex_unlock(&conn->mutex); conn->updated_info = conn->initial_info; ret = nbd_connect(conn->sioc, conn->saddr, conn->do_negotiation ? &conn->updated_info : NULL, conn->tlscreds, conn->tlshostname, &conn->ioc, &local_err); /* * conn->updated_info will finally be returned to the user. Clear the * pointers to our internally allocated strings, which are IN parameters * of nbd_receive_negotiate() and therefore nbd_connect(). Caller * shoudn't be interested in these fields. */ conn->updated_info.x_dirty_bitmap = NULL; conn->updated_info.name = NULL; qemu_mutex_lock(&conn->mutex); error_free(conn->err); conn->err = NULL; error_propagate(&conn->err, local_err); if (ret < 0) { object_unref(OBJECT(conn->sioc)); conn->sioc = NULL; if (conn->do_retry && !conn->detached) { qemu_mutex_unlock(&conn->mutex); sleep(timeout); if (timeout < max_timeout) { timeout *= 2; } qemu_mutex_lock(&conn->mutex); continue; } } break; } /* mutex is locked */ assert(conn->running); conn->running = false; if (conn->wait_co) { aio_co_wake(conn->wait_co); conn->wait_co = NULL; } do_free = conn->detached; qemu_mutex_unlock(&conn->mutex); if (do_free) { nbd_client_connection_do_free(conn); } return NULL; } void nbd_client_connection_release(NBDClientConnection *conn) { bool do_free = false; if (!conn) { return; } WITH_QEMU_LOCK_GUARD(&conn->mutex) { assert(!conn->detached); if (conn->running) { conn->detached = true; } else { do_free = true; } if (conn->sioc) { qio_channel_shutdown(QIO_CHANNEL(conn->sioc), QIO_CHANNEL_SHUTDOWN_BOTH, NULL); } } if (do_free) { nbd_client_connection_do_free(conn); } } /* * Get a new connection in context of @conn: * if the thread is running, wait for completion * if the thread already succeeded in the background, and user didn't get the * result, just return it now * otherwise the thread is not running, so start a thread and wait for * completion * * If @blocking is false, don't wait for the thread, return immediately. * * If @info is not NULL, also do nbd-negotiation after successful connection. * In this case info is used only as out parameter, and is fully initialized by * nbd_co_establish_connection(). "IN" fields of info as well as related only to * nbd_receive_export_list() would be zero (see description of NBDExportInfo in * include/block/nbd.h). */ QIOChannel *coroutine_fn nbd_co_establish_connection(NBDClientConnection *conn, NBDExportInfo *info, bool blocking, Error **errp) { QemuThread thread; if (conn->do_negotiation) { assert(info); } WITH_QEMU_LOCK_GUARD(&conn->mutex) { /* * Don't call nbd_co_establish_connection() in several coroutines in * parallel. Only one call at once is supported. */ assert(!conn->wait_co); if (!conn->running) { if (conn->sioc) { /* Previous attempt finally succeeded in background */ if (conn->do_negotiation) { memcpy(info, &conn->updated_info, sizeof(*info)); if (conn->ioc) { /* TLS channel now has own reference to parent */ object_unref(OBJECT(conn->sioc)); conn->sioc = NULL; return g_steal_pointer(&conn->ioc); } } assert(!conn->ioc); return QIO_CHANNEL(g_steal_pointer(&conn->sioc)); } conn->running = true; qemu_thread_create(&thread, "nbd-connect", connect_thread_func, conn, QEMU_THREAD_DETACHED); } if (!blocking) { if (conn->err) { error_propagate(errp, error_copy(conn->err)); } else { error_setg(errp, "No connection at the moment"); } return NULL; } conn->wait_co = qemu_coroutine_self(); } /* * We are going to wait for connect-thread finish, but * nbd_co_establish_connection_cancel() can interrupt. */ qemu_coroutine_yield(); WITH_QEMU_LOCK_GUARD(&conn->mutex) { if (conn->running) { /* * The connection attempt was canceled and the coroutine resumed * before the connection thread finished its job. Report the * attempt as failed, but leave the connection thread running, * to reuse it for the next connection attempt. */ if (conn->err) { error_propagate(errp, error_copy(conn->err)); } else { /* * The only possible case here is cancelling by open_timer * during nbd_open(). So, the error message is for that case. * If we have more use cases, we can refactor * nbd_co_establish_connection_cancel() to take an additional * parameter cancel_reason, that would be passed than to the * caller of cancelled nbd_co_establish_connection(). */ error_setg(errp, "Connection attempt cancelled by timeout"); } return NULL; } else { /* Thread finished. There must be either error or sioc */ assert(!conn->err != !conn->sioc); if (conn->err) { error_propagate(errp, error_copy(conn->err)); return NULL; } if (conn->do_negotiation) { memcpy(info, &conn->updated_info, sizeof(*info)); if (conn->ioc) { /* TLS channel now has own reference to parent */ object_unref(OBJECT(conn->sioc)); conn->sioc = NULL; return g_steal_pointer(&conn->ioc); } } assert(!conn->ioc); return QIO_CHANNEL(g_steal_pointer(&conn->sioc)); } } abort(); /* unreachable */ } /* * nbd_co_establish_connection_cancel * Cancel nbd_co_establish_connection() asynchronously. * * Note that this function neither directly stops the thread nor closes the * socket, but rather safely wakes nbd_co_establish_connection() which is * sleeping in yield() */ void nbd_co_establish_connection_cancel(NBDClientConnection *conn) { Coroutine *wait_co; WITH_QEMU_LOCK_GUARD(&conn->mutex) { wait_co = g_steal_pointer(&conn->wait_co); } if (wait_co) { aio_co_wake(wait_co); } }