| 1 | /* |
| 2 | * QTest testcases for CPR |
| 3 | * |
| 4 | * Copyright (c) 2016-2018 Red Hat, Inc. and/or its affiliates |
| 5 | * based on the vhost-user-test.c that is: |
| 6 | * Copyright (c) 2014 Virtual Open Systems Sarl. |
| 7 | * |
| 8 | * This work is licensed under the terms of the GNU GPL, version 2 or later. |
| 9 | * See the COPYING file in the top-level directory. |
| 10 | * |
| 11 | */ |
| 12 | |
| 13 | #include "qemu/osdep.h" |
| 14 | #include "libqtest.h" |
| 15 | #include "migration/framework.h" |
| 16 | #include "migration/migration-qmp.h" |
| 17 | #include "migration/migration-util.h" |
| 18 | #include "qapi/error.h" |
| 19 | #include "qobject/qjson.h" |
| 20 | #include "qobject/qlist.h" |
| 21 | |
| 22 | |
| 23 | static char *tmpfs; |
| 24 | |
| 25 | static void *migrate_hook_start_mode_reboot(QTestState *from, QTestState *to) |
| 26 | { |
| 27 | migrate_set_parameter_str(from, "mode", "cpr-reboot"); |
| 28 | migrate_set_parameter_str(to, "mode", "cpr-reboot"); |
| 29 | |
| 30 | return NULL; |
| 31 | } |
| 32 | |
| 33 | static void test_mode_reboot(char *name, MigrateCommon *args) |
| 34 | { |
| 35 | g_autofree char *uri = g_strdup_printf("file:%s/%s", tmpfs, |
| 36 | FILE_TEST_FILENAME); |
| 37 | |
| 38 | args->uri = uri; |
| 39 | args->start_hook = migrate_hook_start_mode_reboot; |
| 40 | |
| 41 | args->start.mem_type = MEM_TYPE_SHMEM; |
| 42 | args->start.caps[MIGRATION_CAPABILITY_X_IGNORE_SHARED] = true; |
| 43 | |
| 44 | test_file_common(args, true); |
| 45 | } |
| 46 | |
| 47 | static int test_transfer(MigrateCommon *args, const char *cpr_channel, |
| 48 | bool incoming_defer) |
| 49 | { |
| 50 | QTestState *from, *to; |
| 51 | QObject *obj, *out_channels = qobject_from_json(args->connect_channels, |
| 52 | &error_abort); |
| 53 | QList *channels_list; |
| 54 | |
| 55 | /* |
| 56 | * The cpr channel must be included in outgoing channels, but not in |
| 57 | * migrate-incoming channels. |
| 58 | */ |
| 59 | channels_list = qobject_to(QList, out_channels); |
| 60 | obj = migrate_str_to_channel(cpr_channel); |
| 61 | qlist_append(channels_list, obj); |
| 62 | |
| 63 | if (migrate_start(&from, &to, &args->start)) { |
| 64 | return -1; |
| 65 | } |
| 66 | |
| 67 | migrate_set_parameter_str(from, "mode", "cpr-transfer"); |
| 68 | |
| 69 | wait_for_serial("src_serial"); |
| 70 | |
| 71 | qtest_qmp_assert_success(from, "{ 'execute' : 'stop'}"); |
| 72 | wait_for_stop(from, get_src()); |
| 73 | migrate_ensure_converge(from); |
| 74 | |
| 75 | migrate_qmp(from, to, NULL, out_channels, "{}"); |
| 76 | |
| 77 | qtest_connect(to); |
| 78 | qtest_qmp_handshake(to, NULL); |
| 79 | if (incoming_defer) { |
| 80 | QObject *in_channels = qobject_from_json(args->connect_channels, |
| 81 | &error_abort); |
| 82 | |
| 83 | migrate_incoming_qmp(to, NULL, in_channels, "{}"); |
| 84 | } |
| 85 | |
| 86 | wait_for_migration_complete(from); |
| 87 | wait_for_migration_complete(to); |
| 88 | |
| 89 | qtest_qmp_assert_success(to, "{ 'execute' : 'cont'}"); |
| 90 | |
| 91 | wait_for_resume(to, get_dst()); |
| 92 | wait_for_serial("dest_serial"); |
| 93 | |
| 94 | migrate_end(from, to, true); |
| 95 | |
| 96 | return 0; |
| 97 | } |
| 98 | |
| 99 | /* |
| 100 | * cpr-transfer mode cannot use the target monitor prior to starting the |
| 101 | * migration, and cannot connect synchronously to the monitor, so defer |
| 102 | * the target connection. |
| 103 | */ |
| 104 | static void test_mode_transfer_common(MigrateCommon *args, bool incoming_defer) |
| 105 | { |
| 106 | g_autofree char *cpr_path = g_strdup_printf("%s/cpr.sock", tmpfs); |
| 107 | g_autofree char *mig_path = g_strdup_printf("%s/migsocket", tmpfs); |
| 108 | g_autofree char *uri = g_strdup_printf("unix:%s", mig_path); |
| 109 | g_autofree char *opts_target = NULL; |
| 110 | |
| 111 | const char *opts = "-machine aux-ram-share=on -nodefaults"; |
| 112 | g_autofree const char *cpr_channel = g_strdup_printf( |
| 113 | "cpr,addr.transport=socket,addr.type=unix,addr.path=%s", |
| 114 | cpr_path); |
| 115 | |
| 116 | g_autofree char *connect_channels = g_strdup_printf( |
| 117 | "[ { 'channel-type': 'main'," |
| 118 | " 'addr': { 'transport': 'socket'," |
| 119 | " 'type': 'unix'," |
| 120 | " 'path': '%s' } } ]", |
| 121 | mig_path); |
| 122 | |
| 123 | /* |
| 124 | * Set up a UNIX domain socket for the CPR channel before |
| 125 | * launching the destination VM, to avoid timing issues |
| 126 | * during connection setup. |
| 127 | */ |
| 128 | int cpr_sockfd = qtest_socket_server(cpr_path); |
| 129 | g_assert(cpr_sockfd >= 0); |
| 130 | |
| 131 | if (incoming_defer) { |
| 132 | opts_target = g_strdup_printf("-incoming cpr,addr.transport=socket," |
| 133 | "addr.type=fd,addr.str=%d %s", |
| 134 | cpr_sockfd, opts); |
| 135 | } else { |
| 136 | opts_target = g_strdup_printf("-incoming %s " |
| 137 | "-incoming cpr,addr.transport=socket," |
| 138 | "addr.type=fd,addr.str=%d %s", |
| 139 | uri, cpr_sockfd, opts); |
| 140 | } |
| 141 | |
| 142 | args->connect_channels = connect_channels; |
| 143 | |
| 144 | args->start.opts_source = opts; |
| 145 | args->start.opts_target = opts_target; |
| 146 | args->start.defer_target_connect = true; |
| 147 | args->start.mem_type = MEM_TYPE_MEMFD; |
| 148 | |
| 149 | if (test_transfer(args, cpr_channel, incoming_defer) < 0) { |
| 150 | close(cpr_sockfd); |
| 151 | unlink(cpr_path); |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | static void test_mode_transfer(char *name, MigrateCommon *args) |
| 156 | { |
| 157 | test_mode_transfer_common(args, false); |
| 158 | } |
| 159 | |
| 160 | static void test_mode_transfer_defer(char *name, MigrateCommon *args) |
| 161 | { |
| 162 | test_mode_transfer_common(args, true); |
| 163 | } |
| 164 | |
| 165 | static void set_cpr_exec_args(QTestState *who, MigrateCommon *args) |
| 166 | { |
| 167 | g_autofree char *qtest_from_args = NULL; |
| 168 | g_autofree char *from_args = NULL; |
| 169 | g_autofree char *to_args = NULL; |
| 170 | g_autofree char *exec_args = NULL; |
| 171 | g_auto(GStrv) argv = NULL; |
| 172 | char *from_str, *src, *dst; |
| 173 | int ret; |
| 174 | |
| 175 | /* |
| 176 | * hide_stderr appends "2>/dev/null" to the command line, but cpr-exec |
| 177 | * passes the command-line words to execv, not to the shell, so suppress it |
| 178 | * here. fd 2 was already bound in the source VM, and execv preserves it. |
| 179 | */ |
| 180 | g_assert(args->start.hide_stderr == false); |
| 181 | |
| 182 | ret = migrate_args(&from_args, &to_args, &args->start); |
| 183 | g_assert(!ret); |
| 184 | qtest_from_args = qtest_qemu_args(from_args); |
| 185 | |
| 186 | /* |
| 187 | * The generated args may have been formatted using "%s %s" with empty |
| 188 | * strings, which can produce consecutive spaces, which g_strsplit would |
| 189 | * convert into empty strings. Ditto for leading and trailing space. |
| 190 | * De-dup spaces to avoid that. |
| 191 | */ |
| 192 | |
| 193 | from_str = src = dst = g_strstrip(qtest_from_args); |
| 194 | do { |
| 195 | if (*src != ' ' || src[-1] != ' ') { |
| 196 | *dst++ = *src; |
| 197 | } |
| 198 | } while (*src++); |
| 199 | |
| 200 | exec_args = g_strconcat(qtest_qemu_binary(migration_get_env()->qemu_dst), |
| 201 | " -incoming defer ", from_str, NULL); |
| 202 | argv = g_strsplit(exec_args, " ", -1); |
| 203 | migrate_set_parameter_strv(who, "cpr-exec-command", argv); |
| 204 | } |
| 205 | |
| 206 | static void wait_for_migration_event(QTestState *who, const char *waitfor) |
| 207 | { |
| 208 | QDict *rsp, *data; |
| 209 | bool done = false; |
| 210 | |
| 211 | while (!done) { |
| 212 | const char *status; |
| 213 | |
| 214 | rsp = qtest_qmp_eventwait_ref(who, "MIGRATION"); |
| 215 | g_assert(qdict_haskey(rsp, "data")); |
| 216 | data = qdict_get_qdict(rsp, "data"); |
| 217 | g_assert(qdict_haskey(data, "status")); |
| 218 | status = qdict_get_str(data, "status"); |
| 219 | g_assert(strcmp(status, "failed")); |
| 220 | done = !strcmp(status, waitfor); |
| 221 | qobject_unref(rsp); |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | static void test_cpr_exec(MigrateCommon *args) |
| 226 | { |
| 227 | QTestState *from, *to; |
| 228 | void *data_hook = NULL; |
| 229 | g_autofree char *connect_uri = g_strdup(args->uri); |
| 230 | g_autofree char *filename = g_strdup_printf("%s/%s", tmpfs, |
| 231 | FILE_TEST_FILENAME); |
| 232 | |
| 233 | if (migrate_start(&from, NULL, &args->start)) { |
| 234 | return; |
| 235 | } |
| 236 | |
| 237 | /* Source and dest never run concurrently */ |
| 238 | g_assert_false(args->live); |
| 239 | |
| 240 | if (args->start_hook) { |
| 241 | data_hook = args->start_hook(from, NULL); |
| 242 | } |
| 243 | |
| 244 | wait_for_serial("src_serial"); |
| 245 | set_cpr_exec_args(from, args); |
| 246 | migrate_set_capability(from, "events", true); |
| 247 | migrate_qmp(from, NULL, connect_uri, NULL, "{}"); |
| 248 | wait_for_migration_event(from, "completed"); |
| 249 | |
| 250 | to = qtest_init_after_exec(from); |
| 251 | |
| 252 | qtest_qmp_assert_success(to, "{ 'execute': 'migrate-incoming'," |
| 253 | " 'arguments': { " |
| 254 | " 'channels': [ { 'channel-type': 'main'," |
| 255 | " 'addr': { 'transport': 'file'," |
| 256 | " 'filename': %s," |
| 257 | " 'offset': 0 } } ] } }", |
| 258 | filename); |
| 259 | wait_for_migration_complete(to); |
| 260 | |
| 261 | wait_for_resume(to, get_dst()); |
| 262 | /* Device on target is still named src_serial because args do not change */ |
| 263 | wait_for_serial("src_serial"); |
| 264 | |
| 265 | if (args->end_hook) { |
| 266 | args->end_hook(from, to, data_hook); |
| 267 | } |
| 268 | |
| 269 | migrate_end(from, to, args->result == MIG_TEST_SUCCEED); |
| 270 | } |
| 271 | |
| 272 | static void *test_mode_exec_start(QTestState *from, QTestState *to) |
| 273 | { |
| 274 | assert(!to); |
| 275 | migrate_set_parameter_str(from, "mode", "cpr-exec"); |
| 276 | return NULL; |
| 277 | } |
| 278 | |
| 279 | static void test_mode_exec(char *name, MigrateCommon *args) |
| 280 | { |
| 281 | g_autofree char *uri = g_strdup_printf("file:%s/%s", tmpfs, |
| 282 | FILE_TEST_FILENAME); |
| 283 | args->uri = uri; |
| 284 | args->start_hook = test_mode_exec_start; |
| 285 | |
| 286 | args->start.only_source = true; |
| 287 | args->start.opts_source = "-machine aux-ram-share=on -nodefaults"; |
| 288 | args->start.mem_type = MEM_TYPE_MEMFD; |
| 289 | |
| 290 | test_cpr_exec(args); |
| 291 | } |
| 292 | |
| 293 | void migration_test_add_cpr(MigrationTestEnv *env) |
| 294 | { |
| 295 | tmpfs = env->tmpfs; |
| 296 | |
| 297 | /* no tests in the smoke set for now */ |
| 298 | |
| 299 | if (!env->full_set) { |
| 300 | return; |
| 301 | } |
| 302 | |
| 303 | /* |
| 304 | * Our CI system has problems with shared memory. |
| 305 | * Don't run this test until we find a workaround. |
| 306 | */ |
| 307 | if (getenv("QEMU_TEST_FLAKY_TESTS")) { |
| 308 | migration_test_add("/migration/mode/reboot", test_mode_reboot); |
| 309 | } |
| 310 | |
| 311 | if (env->has_kvm) { |
| 312 | migration_test_add("/migration/mode/transfer", test_mode_transfer); |
| 313 | migration_test_add("/migration/mode/transfer/defer", |
| 314 | test_mode_transfer_defer); |
| 315 | migration_test_add("/migration/mode/exec", test_mode_exec); |
| 316 | } |
| 317 | } |