]>
Commit | Line | Data |
---|---|---|
f67539c2 TL |
1 | /* |
2 | * rbd-wnbd - RBD in userspace | |
3 | * | |
4 | * Copyright (C) 2020 SUSE LINUX GmbH | |
5 | * | |
6 | * This is free software; you can redistribute it and/or | |
7 | * modify it under the terms of the GNU Lesser General Public | |
8 | * License version 2.1, as published by the Free Software | |
9 | * Foundation. See file COPYING. | |
10 | * | |
11 | */ | |
12 | ||
13 | #include "include/int_types.h" | |
14 | ||
15 | #include <stdio.h> | |
16 | #include <stdlib.h> | |
17 | #include <stddef.h> | |
18 | #include <errno.h> | |
19 | #include <sys/types.h> | |
20 | #include <sys/socket.h> | |
21 | #include <unistd.h> | |
22 | ||
23 | #include <boost/locale/encoding_utf.hpp> | |
24 | ||
25 | #include "wnbd_handler.h" | |
26 | #include "rbd_wnbd.h" | |
27 | ||
28 | #include <fstream> | |
29 | #include <memory> | |
30 | #include <regex> | |
31 | ||
32 | #include "common/Formatter.h" | |
33 | #include "common/TextTable.h" | |
34 | #include "common/ceph_argparse.h" | |
35 | #include "common/config.h" | |
36 | #include "common/debug.h" | |
37 | #include "common/dout.h" | |
38 | #include "common/errno.h" | |
39 | #include "common/version.h" | |
40 | #include "common/win32/service.h" | |
41 | #include "common/admin_socket_client.h" | |
42 | ||
43 | #include "global/global_init.h" | |
44 | ||
45 | #include "include/uuid.h" | |
46 | #include "include/rados/librados.hpp" | |
47 | #include "include/rbd/librbd.hpp" | |
48 | ||
49 | #include <shellapi.h> | |
50 | ||
51 | #define dout_context g_ceph_context | |
52 | #define dout_subsys ceph_subsys_rbd | |
53 | #undef dout_prefix | |
54 | #define dout_prefix *_dout << "rbd-wnbd: " | |
55 | ||
20effc67 | 56 | using namespace std; |
f67539c2 TL |
57 | using boost::locale::conv::utf_to_utf; |
58 | ||
59 | std::wstring to_wstring(const std::string& str) | |
60 | { | |
61 | return utf_to_utf<wchar_t>(str.c_str(), str.c_str() + str.size()); | |
62 | } | |
63 | ||
64 | std::string to_string(const std::wstring& str) | |
65 | { | |
66 | return utf_to_utf<char>(str.c_str(), str.c_str() + str.size()); | |
67 | } | |
68 | ||
69 | bool is_process_running(DWORD pid) | |
70 | { | |
71 | HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid); | |
72 | DWORD ret = WaitForSingleObject(process, 0); | |
73 | CloseHandle(process); | |
74 | return ret == WAIT_TIMEOUT; | |
75 | } | |
76 | ||
77 | DWORD WNBDActiveDiskIterator::fetch_list( | |
78 | PWNBD_CONNECTION_LIST* conn_list) | |
79 | { | |
80 | DWORD curr_buff_sz = 0; | |
81 | DWORD buff_sz = 0; | |
82 | DWORD err = 0; | |
83 | PWNBD_CONNECTION_LIST tmp_list = NULL; | |
84 | ||
85 | // We're using a loop because other connections may show up by the time | |
86 | // we retry. | |
87 | do { | |
88 | if (tmp_list) | |
89 | free(tmp_list); | |
90 | ||
91 | if (buff_sz) { | |
92 | tmp_list = (PWNBD_CONNECTION_LIST) calloc(1, buff_sz); | |
93 | if (!tmp_list) { | |
94 | derr << "Could not allocate " << buff_sz << " bytes." << dendl; | |
95 | err = ERROR_NOT_ENOUGH_MEMORY; | |
96 | break; | |
97 | } | |
98 | } | |
99 | ||
100 | curr_buff_sz = buff_sz; | |
101 | // If the buffer is too small, the return value is 0 and "BufferSize" | |
102 | // will contain the required size. This is counterintuitive, but | |
103 | // Windows drivers can't return a buffer as well as a non-zero status. | |
104 | err = WnbdList(tmp_list, &buff_sz); | |
105 | if (err) | |
106 | break; | |
107 | } while (curr_buff_sz < buff_sz); | |
108 | ||
109 | if (err) { | |
110 | if (tmp_list) | |
111 | free(tmp_list); | |
112 | } else { | |
113 | *conn_list = tmp_list; | |
114 | } | |
115 | return err; | |
116 | } | |
117 | ||
118 | WNBDActiveDiskIterator::WNBDActiveDiskIterator() | |
119 | { | |
120 | DWORD status = WNBDActiveDiskIterator::fetch_list(&conn_list); | |
121 | if (status) { | |
122 | error = EINVAL; | |
123 | } | |
124 | } | |
125 | ||
126 | WNBDActiveDiskIterator::~WNBDActiveDiskIterator() | |
127 | { | |
128 | if (conn_list) { | |
129 | free(conn_list); | |
130 | conn_list = NULL; | |
131 | } | |
132 | } | |
133 | ||
134 | bool WNBDActiveDiskIterator::get(Config *cfg) | |
135 | { | |
136 | index += 1; | |
137 | *cfg = Config(); | |
138 | ||
139 | if (!conn_list || index >= (int)conn_list->Count) { | |
140 | return false; | |
141 | } | |
142 | ||
143 | auto conn_info = conn_list->Connections[index]; | |
144 | auto conn_props = conn_info.Properties; | |
145 | ||
146 | if (strncmp(conn_props.Owner, RBD_WNBD_OWNER_NAME, WNBD_MAX_OWNER_LENGTH)) { | |
147 | dout(10) << "Ignoring disk: " << conn_props.InstanceName | |
148 | << ". Owner: " << conn_props.Owner << dendl; | |
149 | return this->get(cfg); | |
150 | } | |
151 | ||
152 | error = load_mapping_config_from_registry(conn_props.InstanceName, cfg); | |
153 | if (error) { | |
154 | derr << "Could not load registry disk info for: " | |
155 | << conn_props.InstanceName << ". Error: " << error << dendl; | |
156 | return false; | |
157 | } | |
158 | ||
159 | cfg->disk_number = conn_info.DiskNumber; | |
160 | cfg->serial_number = std::string(conn_props.SerialNumber); | |
161 | cfg->pid = conn_props.Pid; | |
162 | cfg->active = cfg->disk_number > 0 && is_process_running(conn_props.Pid); | |
163 | cfg->wnbd_mapped = true; | |
164 | ||
165 | return true; | |
166 | } | |
167 | ||
168 | RegistryDiskIterator::RegistryDiskIterator() | |
169 | { | |
170 | reg_key = new RegistryKey(g_ceph_context, HKEY_LOCAL_MACHINE, | |
171 | SERVICE_REG_KEY, false); | |
172 | if (!reg_key->hKey) { | |
173 | if (!reg_key->missingKey) | |
174 | error = EINVAL; | |
175 | return; | |
176 | } | |
177 | ||
178 | if (RegQueryInfoKey(reg_key->hKey, NULL, NULL, NULL, &subkey_count, | |
179 | NULL, NULL, NULL, NULL, NULL, NULL, NULL)) { | |
180 | derr << "Could not query registry key: " << SERVICE_REG_KEY << dendl; | |
181 | error = EINVAL; | |
182 | return; | |
183 | } | |
184 | } | |
185 | ||
186 | bool RegistryDiskIterator::get(Config *cfg) | |
187 | { | |
188 | index += 1; | |
189 | *cfg = Config(); | |
190 | ||
191 | if (!reg_key->hKey || !subkey_count) { | |
192 | return false; | |
193 | } | |
194 | ||
195 | char subkey_name[MAX_PATH] = {0}; | |
196 | DWORD subkey_name_sz = MAX_PATH; | |
197 | int err = RegEnumKeyEx( | |
198 | reg_key->hKey, index, subkey_name, &subkey_name_sz, | |
199 | NULL, NULL, NULL, NULL); | |
200 | if (err == ERROR_NO_MORE_ITEMS) { | |
201 | return false; | |
202 | } else if (err) { | |
203 | derr << "Could not enumerate registry. Error: " << err << dendl; | |
204 | error = EINVAL; | |
205 | return false; | |
206 | } | |
207 | ||
208 | if (load_mapping_config_from_registry(subkey_name, cfg)) { | |
209 | error = EINVAL; | |
210 | return false; | |
211 | }; | |
212 | ||
213 | return true; | |
214 | } | |
215 | ||
216 | // Iterate over all RBD mappings, getting info from the registry and the driver. | |
217 | bool WNBDDiskIterator::get(Config *cfg) | |
218 | { | |
219 | *cfg = Config(); | |
220 | ||
221 | bool found_active = active_iterator.get(cfg); | |
222 | if (found_active) { | |
223 | active_devices.insert(cfg->devpath); | |
224 | return true; | |
225 | } | |
226 | ||
227 | error = active_iterator.get_error(); | |
228 | if (error) { | |
229 | dout(5) << ": WNBD iterator error: " << error << dendl; | |
230 | return false; | |
231 | } | |
232 | ||
233 | while(registry_iterator.get(cfg)) { | |
234 | if (active_devices.find(cfg->devpath) != active_devices.end()) { | |
235 | // Skip active devices that were already yielded. | |
236 | continue; | |
237 | } | |
238 | return true; | |
239 | } | |
240 | ||
241 | error = registry_iterator.get_error(); | |
242 | if (error) { | |
243 | dout(5) << ": Registry iterator error: " << error << dendl; | |
244 | } | |
245 | return false; | |
246 | } | |
247 | ||
248 | int get_exe_path(std::string& path) { | |
249 | char buffer[MAX_PATH]; | |
250 | DWORD err = 0; | |
251 | ||
252 | int ret = GetModuleFileNameA(NULL, buffer, MAX_PATH); | |
253 | if (!ret || ret == MAX_PATH) { | |
254 | err = GetLastError(); | |
255 | derr << "Could not retrieve executable path. " | |
256 | << "Error: " << win32_strerror(err) << dendl; | |
257 | return -EINVAL; | |
258 | } | |
259 | ||
260 | path = buffer; | |
261 | return 0; | |
262 | } | |
263 | ||
264 | std::string get_cli_args() { | |
265 | std::ostringstream cmdline; | |
266 | for (int i=1; i<__argc; i++) { | |
267 | if (i > 1) | |
268 | cmdline << " "; | |
269 | cmdline << std::quoted(__argv[i]); | |
270 | } | |
271 | return cmdline.str(); | |
272 | } | |
273 | ||
274 | int send_map_request(std::string arguments) { | |
275 | dout(15) << __func__ << ": command arguments: " << arguments << dendl; | |
276 | ||
277 | BYTE request_buff[SERVICE_PIPE_BUFFSZ] = { 0 }; | |
278 | ServiceRequest* request = (ServiceRequest*) request_buff; | |
279 | request->command = Connect; | |
280 | arguments.copy( | |
281 | (char*)request->arguments, | |
282 | SERVICE_PIPE_BUFFSZ - FIELD_OFFSET(ServiceRequest, arguments)); | |
283 | ServiceReply reply = { 0 }; | |
284 | ||
285 | DWORD bytes_read = 0; | |
286 | BOOL success = CallNamedPipe( | |
287 | SERVICE_PIPE_NAME, | |
288 | request_buff, | |
289 | SERVICE_PIPE_BUFFSZ, | |
290 | &reply, | |
291 | sizeof(reply), | |
292 | &bytes_read, | |
293 | DEFAULT_MAP_TIMEOUT_MS); | |
294 | if (!success) { | |
295 | DWORD err = GetLastError(); | |
296 | derr << "Could not send device map request. " | |
297 | << "Make sure that the ceph service is running. " | |
298 | << "Error: " << win32_strerror(err) << dendl; | |
299 | return -EINVAL; | |
300 | } | |
301 | if (reply.status) { | |
302 | derr << "The ceph service failed to map the image. Error: " | |
303 | << reply.status << dendl; | |
304 | } | |
305 | ||
306 | return reply.status; | |
307 | } | |
308 | ||
309 | // Spawn a subprocess using the specified "rbd-wnbd" command | |
310 | // arguments. A pipe is passed to the child process, | |
311 | // which will allow it to communicate the mapping status | |
312 | int map_device_using_suprocess(std::string arguments, int timeout_ms) | |
313 | { | |
314 | STARTUPINFO si; | |
315 | PROCESS_INFORMATION pi; | |
316 | char ch; | |
317 | DWORD err = 0, status = 0; | |
318 | int exit_code = 0; | |
319 | std::ostringstream command_line; | |
320 | std::string exe_path; | |
321 | // Windows async IO context | |
322 | OVERLAPPED connect_o, read_o; | |
323 | HANDLE connect_event = NULL, read_event = NULL; | |
324 | // Used for waiting on multiple events that are going to be initialized later. | |
325 | HANDLE wait_events[2] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}; | |
326 | DWORD bytes_read = 0; | |
327 | // We may get a command line containing an old pipe handle when | |
328 | // recreating mappings, so we'll have to replace it. | |
329 | std::regex pipe_pattern("([\'\"]?--pipe-name[\'\"]? +[\'\"]?[^ ]+[\'\"]?)"); | |
330 | ||
331 | uuid_d uuid; | |
332 | uuid.generate_random(); | |
333 | std::ostringstream pipe_name; | |
334 | pipe_name << "\\\\.\\pipe\\rbd-wnbd-" << uuid; | |
335 | ||
336 | // Create an unique named pipe to communicate with the child. */ | |
337 | HANDLE pipe_handle = CreateNamedPipe( | |
338 | pipe_name.str().c_str(), | |
339 | PIPE_ACCESS_INBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | | |
340 | FILE_FLAG_OVERLAPPED, | |
341 | PIPE_WAIT, | |
342 | 1, // Only accept one instance | |
343 | SERVICE_PIPE_BUFFSZ, | |
344 | SERVICE_PIPE_BUFFSZ, | |
345 | SERVICE_PIPE_TIMEOUT_MS, | |
346 | NULL); | |
347 | if (pipe_handle == INVALID_HANDLE_VALUE) { | |
348 | err = GetLastError(); | |
349 | derr << "CreateNamedPipe failed: " << win32_strerror(err) << dendl; | |
350 | exit_code = -ECHILD; | |
351 | goto finally; | |
352 | } | |
353 | connect_event = CreateEvent(0, TRUE, FALSE, NULL); | |
354 | read_event = CreateEvent(0, TRUE, FALSE, NULL); | |
355 | if (!connect_event || !read_event) { | |
356 | err = GetLastError(); | |
357 | derr << "CreateEvent failed: " << win32_strerror(err) << dendl; | |
358 | exit_code = -ECHILD; | |
359 | goto finally; | |
360 | } | |
361 | connect_o.hEvent = connect_event; | |
362 | read_o.hEvent = read_event; | |
363 | ||
364 | status = ConnectNamedPipe(pipe_handle, &connect_o); | |
365 | err = GetLastError(); | |
366 | if (status || err != ERROR_IO_PENDING) { | |
367 | if (status) | |
368 | err = status; | |
369 | derr << "ConnectNamedPipe failed: " << win32_strerror(err) << dendl; | |
370 | exit_code = -ECHILD; | |
371 | goto finally; | |
372 | } | |
373 | err = 0; | |
374 | ||
375 | dout(5) << __func__ << ": command arguments: " << arguments << dendl; | |
376 | ||
377 | // We'll avoid running arbitrary commands, instead using the executable | |
378 | // path of this process (expected to be the full rbd-wnbd.exe path). | |
379 | err = get_exe_path(exe_path); | |
380 | if (err) { | |
381 | exit_code = -EINVAL; | |
382 | goto finally; | |
383 | } | |
384 | command_line << std::quoted(exe_path) | |
385 | << " " << std::regex_replace(arguments, pipe_pattern, "") | |
386 | << " --pipe-name " << pipe_name.str(); | |
387 | ||
388 | dout(5) << __func__ << ": command line: " << command_line.str() << dendl; | |
389 | ||
390 | GetStartupInfo(&si); | |
391 | // Create a detached child | |
392 | if (!CreateProcess(NULL, (char*)command_line.str().c_str(), | |
393 | NULL, NULL, FALSE, DETACHED_PROCESS, | |
394 | NULL, NULL, &si, &pi)) { | |
395 | err = GetLastError(); | |
396 | derr << "CreateProcess failed: " << win32_strerror(err) << dendl; | |
397 | exit_code = -ECHILD; | |
398 | goto finally; | |
399 | } | |
400 | ||
401 | wait_events[0] = connect_event; | |
402 | wait_events[1] = pi.hProcess; | |
403 | status = WaitForMultipleObjects(2, wait_events, FALSE, timeout_ms); | |
404 | switch(status) { | |
405 | case WAIT_OBJECT_0: | |
406 | if (!GetOverlappedResult(pipe_handle, &connect_o, &bytes_read, TRUE)) { | |
407 | err = GetLastError(); | |
408 | derr << "Couln't establish a connection with the child process. " | |
409 | << "Error: " << win32_strerror(err) << dendl; | |
410 | exit_code = -ECHILD; | |
411 | goto clean_process; | |
412 | } | |
413 | // We have an incoming connection. | |
414 | break; | |
415 | case WAIT_OBJECT_0 + 1: | |
416 | // The process has exited prematurely. | |
417 | goto clean_process; | |
418 | case WAIT_TIMEOUT: | |
419 | derr << "Timed out waiting for child process connection." << dendl; | |
420 | goto clean_process; | |
421 | default: | |
422 | derr << "Failed waiting for child process. Status: " << status << dendl; | |
423 | goto clean_process; | |
424 | } | |
425 | // Block and wait for child to say it is ready. | |
426 | dout(5) << __func__ << ": waiting for child notification." << dendl; | |
427 | if (!ReadFile(pipe_handle, &ch, 1, NULL, &read_o)) { | |
428 | err = GetLastError(); | |
429 | if (err != ERROR_IO_PENDING) { | |
430 | derr << "Receiving child process reply failed with: " | |
431 | << win32_strerror(err) << dendl; | |
432 | exit_code = -ECHILD; | |
433 | goto clean_process; | |
434 | } | |
435 | } | |
436 | wait_events[0] = read_event; | |
437 | wait_events[1] = pi.hProcess; | |
438 | // The RBD daemon is expected to write back right after opening the | |
439 | // pipe. We'll use the same timeout value for now. | |
440 | status = WaitForMultipleObjects(2, wait_events, FALSE, timeout_ms); | |
441 | switch(status) { | |
442 | case WAIT_OBJECT_0: | |
443 | if (!GetOverlappedResult(pipe_handle, &read_o, &bytes_read, TRUE)) { | |
444 | err = GetLastError(); | |
445 | derr << "Receiving child process reply failed with: " | |
446 | << win32_strerror(err) << dendl; | |
447 | exit_code = -ECHILD; | |
448 | goto clean_process; | |
449 | } | |
450 | break; | |
451 | case WAIT_OBJECT_0 + 1: | |
452 | // The process has exited prematurely. | |
453 | goto clean_process; | |
454 | case WAIT_TIMEOUT: | |
455 | derr << "Timed out waiting for child process message." << dendl; | |
456 | goto clean_process; | |
457 | default: | |
458 | derr << "Failed waiting for child process. Status: " << status << dendl; | |
459 | goto clean_process; | |
460 | } | |
461 | ||
462 | dout(5) << __func__ << ": received child notification." << dendl; | |
463 | goto finally; | |
464 | ||
465 | clean_process: | |
466 | if (!is_process_running(pi.dwProcessId)) { | |
467 | GetExitCodeProcess(pi.hProcess, (PDWORD)&exit_code); | |
468 | derr << "Daemon failed with: " << cpp_strerror(exit_code) << dendl; | |
469 | } else { | |
470 | // The process closed the pipe without notifying us or exiting. | |
471 | // This is quite unlikely, but we'll terminate the process. | |
472 | dout(5) << "Terminating unresponsive process." << dendl; | |
473 | TerminateProcess(pi.hProcess, 1); | |
474 | exit_code = -EINVAL; | |
475 | } | |
476 | ||
477 | finally: | |
478 | if (exit_code) | |
479 | derr << "Could not start RBD daemon." << dendl; | |
480 | if (pipe_handle) | |
481 | CloseHandle(pipe_handle); | |
482 | if (connect_event) | |
483 | CloseHandle(connect_event); | |
484 | if (read_event) | |
485 | CloseHandle(read_event); | |
486 | return exit_code; | |
487 | } | |
488 | ||
489 | BOOL WINAPI console_handler_routine(DWORD dwCtrlType) | |
490 | { | |
491 | dout(5) << "Received control signal: " << dwCtrlType | |
492 | << ". Exiting." << dendl; | |
493 | ||
494 | std::unique_lock l{shutdown_lock}; | |
495 | if (handler) | |
496 | handler->shutdown(); | |
497 | ||
498 | return true; | |
499 | } | |
500 | ||
501 | int save_config_to_registry(Config* cfg) | |
502 | { | |
503 | std::string strKey{ SERVICE_REG_KEY }; | |
504 | strKey.append("\\"); | |
505 | strKey.append(cfg->devpath); | |
506 | auto reg_key = RegistryKey( | |
507 | g_ceph_context, HKEY_LOCAL_MACHINE, strKey.c_str(), true); | |
508 | if (!reg_key.hKey) { | |
509 | return -EINVAL; | |
510 | } | |
511 | ||
512 | int ret_val = 0; | |
513 | // Registry writes are immediately available to other processes. | |
514 | // Still, we'll do a flush to ensure that the mapping can be | |
515 | // recreated after a system crash. | |
516 | if (reg_key.set("pid", getpid()) || | |
517 | reg_key.set("devpath", cfg->devpath) || | |
518 | reg_key.set("poolname", cfg->poolname) || | |
519 | reg_key.set("nsname", cfg->nsname) || | |
520 | reg_key.set("imgname", cfg->imgname) || | |
521 | reg_key.set("snapname", cfg->snapname) || | |
522 | reg_key.set("command_line", get_cli_args()) || | |
523 | reg_key.set("persistent", cfg->persistent) || | |
524 | reg_key.set("admin_sock_path", g_conf()->admin_socket) || | |
525 | reg_key.flush()) { | |
526 | ret_val = -EINVAL; | |
527 | } | |
528 | ||
529 | return ret_val; | |
530 | } | |
531 | ||
532 | int remove_config_from_registry(Config* cfg) | |
533 | { | |
534 | std::string strKey{ SERVICE_REG_KEY }; | |
535 | strKey.append("\\"); | |
536 | strKey.append(cfg->devpath); | |
537 | return RegistryKey::remove( | |
538 | g_ceph_context, HKEY_LOCAL_MACHINE, strKey.c_str()); | |
539 | } | |
540 | ||
541 | int load_mapping_config_from_registry(string devpath, Config* cfg) | |
542 | { | |
543 | std::string strKey{ SERVICE_REG_KEY }; | |
544 | strKey.append("\\"); | |
545 | strKey.append(devpath); | |
546 | auto reg_key = RegistryKey( | |
547 | g_ceph_context, HKEY_LOCAL_MACHINE, strKey.c_str(), false); | |
548 | if (!reg_key.hKey) { | |
549 | if (reg_key.missingKey) | |
550 | return -ENOENT; | |
551 | else | |
552 | return -EINVAL; | |
553 | } | |
554 | ||
555 | reg_key.get("devpath", cfg->devpath); | |
556 | reg_key.get("poolname", cfg->poolname); | |
557 | reg_key.get("nsname", cfg->nsname); | |
558 | reg_key.get("imgname", cfg->imgname); | |
559 | reg_key.get("snapname", cfg->snapname); | |
560 | reg_key.get("command_line", cfg->command_line); | |
561 | reg_key.get("persistent", cfg->persistent); | |
562 | reg_key.get("admin_sock_path", cfg->admin_sock_path); | |
563 | ||
564 | return 0; | |
565 | } | |
566 | ||
567 | int restart_registered_mappings( | |
568 | int worker_count, | |
569 | int total_timeout, | |
570 | int image_map_timeout) | |
571 | { | |
572 | Config cfg; | |
573 | WNBDDiskIterator iterator; | |
574 | int err = 0, r; | |
575 | ||
576 | int total_timeout_ms = max(total_timeout, total_timeout * 1000); | |
577 | int image_map_timeout_ms = max(image_map_timeout, image_map_timeout * 1000); | |
578 | ||
579 | LARGE_INTEGER start_t, counter_freq; | |
580 | QueryPerformanceFrequency(&counter_freq); | |
581 | QueryPerformanceCounter(&start_t); | |
582 | ||
583 | boost::asio::thread_pool pool(worker_count); | |
584 | while (iterator.get(&cfg)) { | |
585 | if (cfg.command_line.empty()) { | |
586 | derr << "Could not recreate mapping, missing command line: " | |
587 | << cfg.devpath << dendl; | |
588 | err = -EINVAL; | |
589 | continue; | |
590 | } | |
591 | if (cfg.wnbd_mapped) { | |
592 | dout(5) << __func__ << ": device already mapped: " | |
593 | << cfg.devpath << dendl; | |
594 | continue; | |
595 | } | |
596 | if (!cfg.persistent) { | |
597 | dout(5) << __func__ << ": cleaning up non-persistent mapping: " | |
598 | << cfg.devpath << dendl; | |
599 | r = remove_config_from_registry(&cfg); | |
600 | if (r) { | |
601 | derr << __func__ << ": could not clean up non-persistent mapping: " | |
602 | << cfg.devpath << dendl; | |
603 | } | |
604 | continue; | |
605 | } | |
606 | ||
607 | boost::asio::post(pool, | |
608 | [&, cfg]() mutable | |
609 | { | |
610 | LARGE_INTEGER curr_t, elapsed_ms; | |
611 | QueryPerformanceCounter(&curr_t); | |
612 | elapsed_ms.QuadPart = curr_t.QuadPart - start_t.QuadPart; | |
613 | elapsed_ms.QuadPart *= 1000; | |
614 | elapsed_ms.QuadPart /= counter_freq.QuadPart; | |
615 | ||
616 | int time_left_ms = max( | |
617 | 0, | |
618 | total_timeout_ms - (int)elapsed_ms.QuadPart); | |
619 | time_left_ms = min(image_map_timeout_ms, time_left_ms); | |
620 | if (!time_left_ms) { | |
621 | err = -ETIMEDOUT; | |
622 | return; | |
623 | } | |
624 | ||
625 | dout(5) << "Remapping: " << cfg.devpath | |
626 | << ". Timeout: " << time_left_ms << " ms." << dendl; | |
627 | ||
628 | // We'll try to map all devices and return a non-zero value | |
629 | // if any of them fails. | |
630 | r = map_device_using_suprocess(cfg.command_line, time_left_ms); | |
631 | if (r) { | |
632 | err = r; | |
633 | derr << "Could not create mapping: " | |
634 | << cfg.devpath << ". Error: " << r << dendl; | |
635 | } else { | |
636 | dout(5) << "Successfully remapped: " << cfg.devpath << dendl; | |
637 | } | |
638 | }); | |
639 | } | |
640 | pool.join(); | |
641 | ||
642 | r = iterator.get_error(); | |
643 | if (r) { | |
644 | derr << "Could not fetch all mappings. Error: " << r << dendl; | |
645 | err = r; | |
646 | } | |
647 | ||
648 | return err; | |
649 | } | |
650 | ||
651 | int disconnect_all_mappings( | |
652 | bool unregister, | |
653 | bool hard_disconnect, | |
654 | int soft_disconnect_timeout, | |
655 | int worker_count) | |
656 | { | |
657 | // Although not generally recommended, soft_disconnect_timeout can be 0, | |
658 | // which means infinite timeout. | |
659 | ceph_assert(soft_disconnect_timeout >= 0); | |
660 | ceph_assert(worker_count > 0); | |
661 | int64_t timeout_ms = soft_disconnect_timeout * 1000; | |
662 | ||
663 | Config cfg; | |
664 | WNBDActiveDiskIterator iterator; | |
665 | int err = 0, r; | |
666 | ||
667 | boost::asio::thread_pool pool(worker_count); | |
668 | LARGE_INTEGER start_t, counter_freq; | |
669 | QueryPerformanceFrequency(&counter_freq); | |
670 | QueryPerformanceCounter(&start_t); | |
671 | while (iterator.get(&cfg)) { | |
672 | boost::asio::post(pool, | |
673 | [&, cfg]() mutable | |
674 | { | |
675 | LARGE_INTEGER curr_t, elapsed_ms; | |
676 | QueryPerformanceCounter(&curr_t); | |
677 | elapsed_ms.QuadPart = curr_t.QuadPart - start_t.QuadPart; | |
678 | elapsed_ms.QuadPart *= 1000; | |
679 | elapsed_ms.QuadPart /= counter_freq.QuadPart; | |
680 | ||
681 | int64_t time_left_ms = max((int64_t)0, timeout_ms - elapsed_ms.QuadPart); | |
682 | ||
683 | cfg.hard_disconnect = hard_disconnect || !time_left_ms; | |
684 | cfg.hard_disconnect_fallback = true; | |
685 | cfg.soft_disconnect_timeout = time_left_ms / 1000; | |
686 | ||
687 | dout(5) << "Removing mapping: " << cfg.devpath | |
688 | << ". Timeout: " << cfg.soft_disconnect_timeout | |
689 | << "s. Hard disconnect: " << cfg.hard_disconnect | |
690 | << dendl; | |
691 | ||
692 | r = do_unmap(&cfg, unregister); | |
693 | if (r) { | |
694 | err = r; | |
695 | derr << "Could not remove mapping: " << cfg.devpath | |
696 | << ". Error: " << r << dendl; | |
697 | } else { | |
698 | dout(5) << "Successfully removed mapping: " << cfg.devpath << dendl; | |
699 | } | |
700 | }); | |
701 | } | |
702 | pool.join(); | |
703 | ||
704 | r = iterator.get_error(); | |
705 | if (r) { | |
706 | derr << "Could not fetch all mappings. Error: " << r << dendl; | |
707 | err = r; | |
708 | } | |
709 | ||
710 | return err; | |
711 | } | |
712 | ||
713 | class RBDService : public ServiceBase { | |
714 | private: | |
715 | bool hard_disconnect; | |
716 | int soft_disconnect_timeout; | |
717 | int thread_count; | |
718 | int service_start_timeout; | |
719 | int image_map_timeout; | |
720 | bool remap_failure_fatal; | |
721 | ||
722 | public: | |
723 | RBDService(bool _hard_disconnect, | |
724 | int _soft_disconnect_timeout, | |
725 | int _thread_count, | |
726 | int _service_start_timeout, | |
727 | int _image_map_timeout, | |
728 | bool _remap_failure_fatal) | |
729 | : ServiceBase(g_ceph_context) | |
730 | , hard_disconnect(_hard_disconnect) | |
731 | , soft_disconnect_timeout(_soft_disconnect_timeout) | |
732 | , thread_count(_thread_count) | |
733 | , service_start_timeout(_service_start_timeout) | |
734 | , image_map_timeout(_image_map_timeout) | |
735 | , remap_failure_fatal(_remap_failure_fatal) | |
736 | { | |
737 | } | |
738 | ||
739 | static int execute_command(ServiceRequest* request) | |
740 | { | |
741 | switch(request->command) { | |
742 | case Connect: | |
743 | dout(5) << "Received device connect request. Command line: " | |
744 | << (char*)request->arguments << dendl; | |
745 | // TODO: use the configured service map timeout. | |
746 | // TODO: add ceph.conf options. | |
747 | return map_device_using_suprocess( | |
748 | (char*)request->arguments, DEFAULT_MAP_TIMEOUT_MS); | |
749 | default: | |
750 | dout(5) << "Received unsupported command: " | |
751 | << request->command << dendl; | |
752 | return -ENOSYS; | |
753 | } | |
754 | } | |
755 | ||
756 | static DWORD handle_connection(HANDLE pipe_handle) | |
757 | { | |
758 | PBYTE message[SERVICE_PIPE_BUFFSZ] = { 0 }; | |
759 | DWORD bytes_read = 0, bytes_written = 0; | |
760 | DWORD err = 0; | |
761 | DWORD reply_sz = 0; | |
762 | ServiceReply reply = { 0 }; | |
763 | ||
764 | dout(20) << __func__ << ": Receiving message." << dendl; | |
765 | BOOL success = ReadFile( | |
766 | pipe_handle, message, SERVICE_PIPE_BUFFSZ, | |
767 | &bytes_read, NULL); | |
768 | if (!success || !bytes_read) { | |
769 | err = GetLastError(); | |
770 | derr << "Could not read service command: " | |
771 | << win32_strerror(err) << dendl; | |
772 | goto exit; | |
773 | } | |
774 | ||
775 | dout(20) << __func__ << ": Executing command." << dendl; | |
776 | reply.status = execute_command((ServiceRequest*) message); | |
777 | reply_sz = sizeof(reply); | |
778 | ||
779 | dout(20) << __func__ << ": Sending reply. Status: " | |
780 | << reply.status << dendl; | |
781 | success = WriteFile( | |
782 | pipe_handle, &reply, reply_sz, &bytes_written, NULL); | |
783 | if (!success || reply_sz != bytes_written) { | |
784 | err = GetLastError(); | |
785 | derr << "Could not send service command result: " | |
786 | << win32_strerror(err) << dendl; | |
787 | } | |
788 | ||
789 | exit: | |
790 | dout(20) << __func__ << ": Cleaning up connection." << dendl; | |
791 | FlushFileBuffers(pipe_handle); | |
792 | DisconnectNamedPipe(pipe_handle); | |
793 | CloseHandle(pipe_handle); | |
794 | ||
795 | return err; | |
796 | } | |
797 | ||
798 | // We have to support Windows server 2016. Unix sockets only work on | |
799 | // WS 2019, so we can't use the Ceph admin socket abstraction. | |
800 | // Getting the Ceph admin sockets to work with Windows named pipes | |
801 | // would require quite a few changes. | |
802 | static DWORD accept_pipe_connection() { | |
803 | DWORD err = 0; | |
804 | // We're currently using default ACLs, which grant full control to the | |
805 | // LocalSystem account and administrator as well as the owner. | |
806 | dout(20) << __func__ << ": opening new pipe instance" << dendl; | |
807 | HANDLE pipe_handle = CreateNamedPipe( | |
808 | SERVICE_PIPE_NAME, | |
809 | PIPE_ACCESS_DUPLEX, | |
810 | PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, | |
811 | PIPE_UNLIMITED_INSTANCES, | |
812 | SERVICE_PIPE_BUFFSZ, | |
813 | SERVICE_PIPE_BUFFSZ, | |
814 | SERVICE_PIPE_TIMEOUT_MS, | |
815 | NULL); | |
816 | if (pipe_handle == INVALID_HANDLE_VALUE) { | |
817 | err = GetLastError(); | |
818 | derr << "CreatePipe failed: " << win32_strerror(err) << dendl; | |
819 | return -EINVAL; | |
820 | } | |
821 | ||
822 | dout(20) << __func__ << ": waiting for connections." << dendl; | |
823 | BOOL connected = ConnectNamedPipe(pipe_handle, NULL); | |
824 | if (!connected) { | |
825 | err = GetLastError(); | |
826 | if (err != ERROR_PIPE_CONNECTED) { | |
827 | derr << "Pipe connection failed: " << win32_strerror(err) << dendl; | |
828 | ||
829 | CloseHandle(pipe_handle); | |
830 | return err; | |
831 | } | |
832 | } | |
833 | ||
834 | dout(20) << __func__ << ": Connection received." << dendl; | |
835 | // We'll handle the connection in a separate thread and at the same time | |
836 | // accept a new connection. | |
837 | HANDLE handler_thread = CreateThread( | |
838 | NULL, 0, (LPTHREAD_START_ROUTINE) handle_connection, pipe_handle, 0, 0); | |
839 | if (!handler_thread) { | |
840 | err = GetLastError(); | |
841 | derr << "Could not start pipe connection handler thread: " | |
842 | << win32_strerror(err) << dendl; | |
843 | CloseHandle(pipe_handle); | |
844 | } else { | |
845 | CloseHandle(handler_thread); | |
846 | } | |
847 | ||
848 | return err; | |
849 | } | |
850 | ||
851 | static int pipe_server_loop(LPVOID arg) | |
852 | { | |
853 | dout(5) << "Accepting admin pipe connections." << dendl; | |
854 | while (1) { | |
855 | // This call will block until a connection is received, which will | |
856 | // then be handled in a separate thread. The function returns, allowing | |
857 | // us to accept another simultaneous connection. | |
858 | accept_pipe_connection(); | |
859 | } | |
860 | return 0; | |
861 | } | |
862 | ||
863 | int create_pipe_server() { | |
864 | HANDLE handler_thread = CreateThread( | |
865 | NULL, 0, (LPTHREAD_START_ROUTINE) pipe_server_loop, NULL, 0, 0); | |
866 | DWORD err = 0; | |
867 | ||
868 | if (!handler_thread) { | |
869 | err = GetLastError(); | |
870 | derr << "Could not start pipe server: " << win32_strerror(err) << dendl; | |
871 | } else { | |
872 | CloseHandle(handler_thread); | |
873 | } | |
874 | ||
875 | return err; | |
876 | } | |
877 | ||
878 | int run_hook() override { | |
879 | // Restart registered mappings before accepting new ones. | |
880 | int r = restart_registered_mappings( | |
881 | thread_count, service_start_timeout, image_map_timeout); | |
882 | if (r) { | |
883 | if (remap_failure_fatal) { | |
884 | derr << "Couldn't remap all images. Cleaning up." << dendl; | |
885 | return r; | |
886 | } else { | |
887 | dout(0) << "Ignoring image remap failure." << dendl; | |
888 | } | |
889 | } | |
890 | ||
891 | return create_pipe_server(); | |
892 | } | |
893 | ||
894 | // Invoked when the service is requested to stop. | |
895 | int stop_hook() override { | |
896 | return disconnect_all_mappings( | |
897 | false, hard_disconnect, soft_disconnect_timeout, thread_count); | |
898 | } | |
899 | // Invoked when the system is shutting down. | |
900 | int shutdown_hook() override { | |
901 | return stop_hook(); | |
902 | } | |
903 | }; | |
904 | ||
905 | static void usage() | |
906 | { | |
907 | const char* usage_str =R"( | |
908 | Usage: rbd-wnbd [options] map <image-or-snap-spec> Map an image to wnbd device | |
909 | [options] unmap <device|image-or-snap-spec> Unmap wnbd device | |
910 | [options] list List mapped wnbd devices | |
911 | [options] show <image-or-snap-spec> Show mapped wnbd device | |
912 | stats <image-or-snap-spec> Show IO counters | |
913 | [options] service Windows service entrypoint, | |
914 | handling device lifecycle | |
915 | ||
916 | Map options: | |
917 | --device <device path> Optional mapping unique identifier | |
918 | --exclusive Forbid writes by other clients | |
919 | --read-only Map read-only | |
920 | --non-persistent Do not recreate the mapping when the Ceph service | |
921 | restarts. By default, mappings are persistent | |
922 | --io-req-workers The number of workers that dispatch IO requests. | |
923 | Default: 4 | |
924 | --io-reply-workers The number of workers that dispatch IO replies. | |
925 | Default: 4 | |
926 | ||
927 | Unmap options: | |
928 | --hard-disconnect Skip attempting a soft disconnect | |
929 | --no-hard-disconnect-fallback Immediately return an error if the soft | |
930 | disconnect fails instead of attempting a hard | |
931 | disconnect as fallback | |
932 | --soft-disconnect-timeout Soft disconnect timeout in seconds. The soft | |
933 | disconnect operation uses PnP to notify the | |
934 | Windows storage stack that the device is going to | |
935 | be disconnectd. Storage drivers can block this | |
936 | operation if there are pending operations, | |
937 | unflushed caches or open handles. Default: 15 | |
938 | ||
939 | Service options: | |
940 | --hard-disconnect Skip attempting a soft disconnect | |
941 | --soft-disconnect-timeout Cummulative soft disconnect timeout in seconds, | |
942 | used when disconnecting existing mappings. A hard | |
943 | disconnect will be issued when hitting the timeout | |
944 | --service-thread-count The number of workers used when mapping or | |
945 | unmapping images. Default: 8 | |
946 | --start-timeout The service start timeout in seconds. Default: 120 | |
947 | --map-timeout Individual image map timeout in seconds. Default: 20 | |
948 | --remap-failure-fatal If set, the service will stop when failing to remap | |
949 | an image at start time, unmapping images that have | |
950 | been mapped so far. | |
951 | ||
952 | Show|List options: | |
953 | --format plain|json|xml Output format (default: plain) | |
954 | --pretty-format Pretty formatting (json and xml) | |
955 | ||
956 | Common options: | |
957 | --wnbd-log-level libwnbd.dll log level | |
958 | ||
959 | )"; | |
960 | ||
961 | std::cout << usage_str; | |
962 | generic_server_usage(); | |
963 | } | |
964 | ||
965 | ||
966 | static Command cmd = None; | |
967 | ||
968 | int construct_devpath_if_missing(Config* cfg) | |
969 | { | |
970 | // Windows doesn't allow us to request specific disk paths when mapping an | |
971 | // image. This will just be used by rbd-wnbd and wnbd as an identifier. | |
972 | if (cfg->devpath.empty()) { | |
973 | if (cfg->imgname.empty()) { | |
974 | derr << "Missing image name." << dendl; | |
975 | return -EINVAL; | |
976 | } | |
977 | ||
978 | if (!cfg->poolname.empty()) { | |
979 | cfg->devpath += cfg->poolname; | |
980 | cfg->devpath += '/'; | |
981 | } | |
982 | if (!cfg->nsname.empty()) { | |
983 | cfg->devpath += cfg->nsname; | |
984 | cfg->devpath += '/'; | |
985 | } | |
986 | ||
987 | cfg->devpath += cfg->imgname; | |
988 | ||
989 | if (!cfg->snapname.empty()) { | |
990 | cfg->devpath += '@'; | |
991 | cfg->devpath += cfg->snapname; | |
992 | } | |
993 | } | |
994 | ||
995 | return 0; | |
996 | } | |
997 | ||
998 | boost::intrusive_ptr<CephContext> do_global_init( | |
999 | int argc, const char *argv[], Config *cfg) | |
1000 | { | |
20effc67 | 1001 | auto args = argv_to_vec(argc, argv); |
f67539c2 TL |
1002 | |
1003 | code_environment_t code_env; | |
1004 | int flags; | |
1005 | ||
1006 | switch(cmd) { | |
1007 | case Connect: | |
1008 | code_env = CODE_ENVIRONMENT_DAEMON; | |
1009 | flags = CINIT_FLAG_UNPRIVILEGED_DAEMON_DEFAULTS; | |
1010 | break; | |
1011 | case Service: | |
1012 | code_env = CODE_ENVIRONMENT_DAEMON; | |
1013 | flags = CINIT_FLAG_UNPRIVILEGED_DAEMON_DEFAULTS | | |
1014 | CINIT_FLAG_NO_MON_CONFIG | | |
1015 | CINIT_FLAG_NO_DAEMON_ACTIONS; | |
1016 | break; | |
1017 | default: | |
1018 | code_env = CODE_ENVIRONMENT_UTILITY; | |
1019 | flags = CINIT_FLAG_NO_MON_CONFIG; | |
1020 | break; | |
1021 | } | |
1022 | ||
1023 | global_pre_init(NULL, args, CEPH_ENTITY_TYPE_CLIENT, code_env, flags); | |
1024 | // Avoid cluttering the console when spawning a mapping that will run | |
1025 | // in the background. | |
1026 | if (g_conf()->daemonize && cfg->parent_pipe.empty()) { | |
1027 | flags |= CINIT_FLAG_NO_DAEMON_ACTIONS; | |
1028 | } | |
1029 | auto cct = global_init(NULL, args, CEPH_ENTITY_TYPE_CLIENT, | |
1030 | code_env, flags, FALSE); | |
1031 | ||
1032 | // There's no fork on Windows, we should be safe calling this anytime. | |
1033 | common_init_finish(g_ceph_context); | |
1034 | global_init_chdir(g_ceph_context); | |
1035 | ||
1036 | return cct; | |
1037 | } | |
1038 | ||
1039 | static int do_map(Config *cfg) | |
1040 | { | |
1041 | int r; | |
1042 | ||
1043 | librados::Rados rados; | |
1044 | librbd::RBD rbd; | |
1045 | librados::IoCtx io_ctx; | |
1046 | librbd::Image image; | |
1047 | librbd::image_info_t info; | |
1048 | HANDLE parent_pipe_handle = INVALID_HANDLE_VALUE; | |
1049 | int err = 0; | |
1050 | ||
1051 | if (g_conf()->daemonize && cfg->parent_pipe.empty()) { | |
1052 | return send_map_request(get_cli_args()); | |
1053 | } | |
1054 | ||
1055 | dout(0) << "Mapping RBD image: " << cfg->devpath << dendl; | |
1056 | ||
1057 | r = rados.init_with_context(g_ceph_context); | |
1058 | if (r < 0) { | |
1059 | derr << "rbd-wnbd: couldn't initialize rados: " << cpp_strerror(r) | |
1060 | << dendl; | |
1061 | goto close_ret; | |
1062 | } | |
1063 | ||
1064 | r = rados.connect(); | |
1065 | if (r < 0) { | |
1066 | derr << "rbd-wnbd: couldn't connect to rados: " << cpp_strerror(r) | |
1067 | << dendl; | |
1068 | goto close_ret; | |
1069 | } | |
1070 | ||
1071 | r = rados.ioctx_create(cfg->poolname.c_str(), io_ctx); | |
1072 | if (r < 0) { | |
1073 | derr << "rbd-wnbd: couldn't create IO context: " << cpp_strerror(r) | |
1074 | << dendl; | |
1075 | goto close_ret; | |
1076 | } | |
1077 | ||
1078 | io_ctx.set_namespace(cfg->nsname); | |
1079 | ||
1080 | r = rbd.open(io_ctx, image, cfg->imgname.c_str()); | |
1081 | if (r < 0) { | |
1082 | derr << "rbd-wnbd: couldn't open rbd image: " << cpp_strerror(r) | |
1083 | << dendl; | |
1084 | goto close_ret; | |
1085 | } | |
1086 | ||
1087 | if (cfg->exclusive) { | |
1088 | r = image.lock_acquire(RBD_LOCK_MODE_EXCLUSIVE); | |
1089 | if (r < 0) { | |
1090 | derr << "rbd-wnbd: failed to acquire exclusive lock: " << cpp_strerror(r) | |
1091 | << dendl; | |
1092 | goto close_ret; | |
1093 | } | |
1094 | } | |
1095 | ||
1096 | if (!cfg->snapname.empty()) { | |
1097 | r = image.snap_set(cfg->snapname.c_str()); | |
1098 | if (r < 0) { | |
1099 | derr << "rbd-wnbd: couldn't use snapshot: " << cpp_strerror(r) | |
1100 | << dendl; | |
1101 | goto close_ret; | |
1102 | } | |
1103 | } | |
1104 | ||
1105 | r = image.stat(info, sizeof(info)); | |
1106 | if (r < 0) | |
1107 | goto close_ret; | |
1108 | ||
1109 | if (info.size > _UI64_MAX) { | |
1110 | r = -EFBIG; | |
1111 | derr << "rbd-wnbd: image is too large (" << byte_u_t(info.size) | |
1112 | << ", max is " << byte_u_t(_UI64_MAX) << ")" << dendl; | |
1113 | goto close_ret; | |
1114 | } | |
1115 | ||
1116 | // We're storing mapping details in the registry even for non-persistent | |
1117 | // mappings. This allows us to easily retrieve mapping details such | |
1118 | // as the rbd pool or admin socket path. | |
1119 | // We're cleaning up the registry entry when the non-persistent mapping | |
1120 | // gets disconnected or when the ceph service restarts. | |
1121 | r = save_config_to_registry(cfg); | |
1122 | if (r < 0) | |
1123 | goto close_ret; | |
1124 | ||
1125 | handler = new WnbdHandler(image, cfg->devpath, | |
1126 | info.size / RBD_WNBD_BLKSIZE, | |
1127 | RBD_WNBD_BLKSIZE, | |
1128 | !cfg->snapname.empty() || cfg->readonly, | |
1129 | g_conf().get_val<bool>("rbd_cache"), | |
1130 | cfg->io_req_workers, | |
1131 | cfg->io_reply_workers); | |
1132 | r = handler->start(); | |
1133 | if (r) { | |
1134 | r = r == ERROR_ALREADY_EXISTS ? -EEXIST : -EINVAL; | |
1135 | goto close_ret; | |
1136 | } | |
1137 | ||
1138 | // We're informing the parent processes that the initialization | |
1139 | // was successful. | |
1140 | if (!cfg->parent_pipe.empty()) { | |
1141 | parent_pipe_handle = CreateFile( | |
1142 | cfg->parent_pipe.c_str(), GENERIC_WRITE, 0, NULL, | |
1143 | OPEN_EXISTING, 0, NULL); | |
1144 | if (parent_pipe_handle == INVALID_HANDLE_VALUE) { | |
1145 | derr << "Could not open parent pipe: " << win32_strerror(err) << dendl; | |
1146 | } else if (!WriteFile(parent_pipe_handle, "a", 1, NULL, NULL)) { | |
1147 | // TODO: consider exiting in this case. The parent didn't wait for us, | |
1148 | // maybe it was killed after a timeout. | |
1149 | err = GetLastError(); | |
1150 | derr << "Failed to communicate with the parent: " | |
1151 | << win32_strerror(err) << dendl; | |
1152 | } else { | |
1153 | dout(5) << __func__ << ": submitted parent notification." << dendl; | |
1154 | } | |
1155 | ||
1156 | if (parent_pipe_handle != INVALID_HANDLE_VALUE) | |
1157 | CloseHandle(parent_pipe_handle); | |
1158 | ||
1159 | global_init_postfork_finish(g_ceph_context); | |
1160 | } | |
1161 | ||
1162 | handler->wait(); | |
1163 | handler->shutdown(); | |
1164 | ||
1165 | // The registry record shouldn't be removed for (already) running mappings. | |
1166 | if (!cfg->persistent) { | |
1167 | dout(5) << __func__ << ": cleaning up non-persistent mapping: " | |
1168 | << cfg->devpath << dendl; | |
1169 | r = remove_config_from_registry(cfg); | |
1170 | if (r) { | |
1171 | derr << __func__ << ": could not clean up non-persistent mapping: " | |
1172 | << cfg->devpath << dendl; | |
1173 | } | |
1174 | } | |
1175 | ||
1176 | close_ret: | |
1177 | std::unique_lock l{shutdown_lock}; | |
1178 | ||
1179 | image.close(); | |
1180 | io_ctx.close(); | |
1181 | rados.shutdown(); | |
1182 | if (handler) { | |
1183 | delete handler; | |
1184 | handler = nullptr; | |
1185 | } | |
1186 | ||
1187 | return r; | |
1188 | } | |
1189 | ||
1190 | static int do_unmap(Config *cfg, bool unregister) | |
1191 | { | |
1192 | WNBD_REMOVE_OPTIONS remove_options = {0}; | |
1193 | remove_options.Flags.HardRemove = cfg->hard_disconnect; | |
1194 | remove_options.Flags.HardRemoveFallback = cfg->hard_disconnect_fallback; | |
1195 | remove_options.SoftRemoveTimeoutMs = cfg->soft_disconnect_timeout * 1000; | |
1196 | remove_options.SoftRemoveRetryIntervalMs = SOFT_REMOVE_RETRY_INTERVAL * 1000; | |
1197 | ||
1198 | int err = WnbdRemoveEx(cfg->devpath.c_str(), &remove_options); | |
1199 | if (err && err != ERROR_FILE_NOT_FOUND) { | |
1200 | return -EINVAL; | |
1201 | } | |
1202 | ||
1203 | if (unregister) { | |
1204 | err = remove_config_from_registry(cfg); | |
1205 | if (err) { | |
1206 | derr << "rbd-wnbd: failed to unregister device: " | |
1207 | << cfg->devpath << ". Error: " << err << dendl; | |
1208 | return -EINVAL; | |
1209 | } | |
1210 | } | |
1211 | return 0; | |
1212 | } | |
1213 | ||
1214 | static int parse_imgpath(const std::string &imgpath, Config *cfg, | |
1215 | std::ostream *err_msg) | |
1216 | { | |
1217 | std::regex pattern("^(?:([^/]+)/(?:([^/@]+)/)?)?([^@]+)(?:@([^/@]+))?$"); | |
1218 | std::smatch match; | |
1219 | if (!std::regex_match(imgpath, match, pattern)) { | |
1220 | derr << "rbd-wnbd: invalid spec '" << imgpath << "'" << dendl; | |
1221 | return -EINVAL; | |
1222 | } | |
1223 | ||
1224 | if (match[1].matched) { | |
1225 | cfg->poolname = match[1]; | |
1226 | } | |
1227 | ||
1228 | if (match[2].matched) { | |
1229 | cfg->nsname = match[2]; | |
1230 | } | |
1231 | ||
1232 | cfg->imgname = match[3]; | |
1233 | ||
1234 | if (match[4].matched) | |
1235 | cfg->snapname = match[4]; | |
1236 | ||
1237 | return 0; | |
1238 | } | |
1239 | ||
1240 | static int do_list_mapped_devices(const std::string &format, bool pretty_format) | |
1241 | { | |
1242 | std::unique_ptr<ceph::Formatter> f; | |
1243 | TextTable tbl; | |
1244 | ||
1245 | if (format == "json") { | |
1246 | f.reset(new JSONFormatter(pretty_format)); | |
1247 | } else if (format == "xml") { | |
1248 | f.reset(new XMLFormatter(pretty_format)); | |
1249 | } else if (!format.empty() && format != "plain") { | |
1250 | derr << "rbd-wnbd: invalid output format: " << format << dendl; | |
1251 | return -EINVAL; | |
1252 | } | |
1253 | ||
1254 | if (f) { | |
1255 | f->open_array_section("devices"); | |
1256 | } else { | |
1257 | tbl.define_column("id", TextTable::LEFT, TextTable::LEFT); | |
1258 | tbl.define_column("pool", TextTable::LEFT, TextTable::LEFT); | |
1259 | tbl.define_column("namespace", TextTable::LEFT, TextTable::LEFT); | |
1260 | tbl.define_column("image", TextTable::LEFT, TextTable::LEFT); | |
1261 | tbl.define_column("snap", TextTable::LEFT, TextTable::LEFT); | |
1262 | tbl.define_column("device", TextTable::LEFT, TextTable::LEFT); | |
1263 | tbl.define_column("disk_number", TextTable::LEFT, TextTable::LEFT); | |
1264 | tbl.define_column("status", TextTable::LEFT, TextTable::LEFT); | |
1265 | } | |
1266 | ||
1267 | Config cfg; | |
1268 | WNBDDiskIterator wnbd_disk_iterator; | |
1269 | ||
1270 | while (wnbd_disk_iterator.get(&cfg)) { | |
1271 | const char* status = cfg.active ? | |
1272 | WNBD_STATUS_ACTIVE : WNBD_STATUS_INACTIVE; | |
1273 | ||
1274 | if (f) { | |
1275 | f->open_object_section("device"); | |
1276 | f->dump_int("id", cfg.pid ? cfg.pid : -1); | |
1277 | f->dump_string("device", cfg.devpath); | |
1278 | f->dump_string("pool", cfg.poolname); | |
1279 | f->dump_string("namespace", cfg.nsname); | |
1280 | f->dump_string("image", cfg.imgname); | |
1281 | f->dump_string("snap", cfg.snapname); | |
1282 | f->dump_int("disk_number", cfg.disk_number ? cfg.disk_number : -1); | |
1283 | f->dump_string("status", status); | |
1284 | f->close_section(); | |
1285 | } else { | |
1286 | if (cfg.snapname.empty()) { | |
1287 | cfg.snapname = "-"; | |
1288 | } | |
1289 | tbl << (cfg.pid ? cfg.pid : -1) << cfg.poolname << cfg.nsname | |
1290 | << cfg.imgname << cfg.snapname << cfg.devpath | |
1291 | << cfg.disk_number << status << TextTable::endrow; | |
1292 | } | |
1293 | } | |
1294 | int error = wnbd_disk_iterator.get_error(); | |
1295 | if (error) { | |
1296 | derr << "Could not get disk list: " << error << dendl; | |
1297 | return -error; | |
1298 | } | |
1299 | ||
1300 | if (f) { | |
1301 | f->close_section(); | |
1302 | f->flush(std::cout); | |
1303 | } else { | |
1304 | std::cout << tbl; | |
1305 | } | |
1306 | ||
1307 | return 0; | |
1308 | } | |
1309 | ||
1310 | static int do_show_mapped_device(std::string format, bool pretty_format, | |
1311 | std::string devpath) | |
1312 | { | |
1313 | std::unique_ptr<ceph::Formatter> f; | |
1314 | TextTable tbl; | |
1315 | ||
1316 | if (format.empty() || format == "plain") { | |
1317 | format = "json"; | |
1318 | pretty_format = true; | |
1319 | } | |
1320 | if (format == "json") { | |
1321 | f.reset(new JSONFormatter(pretty_format)); | |
1322 | } else if (format == "xml") { | |
1323 | f.reset(new XMLFormatter(pretty_format)); | |
1324 | } else { | |
1325 | derr << "rbd-wnbd: invalid output format: " << format << dendl; | |
1326 | return -EINVAL; | |
1327 | } | |
1328 | ||
1329 | Config cfg; | |
1330 | int error = load_mapping_config_from_registry(devpath, &cfg); | |
1331 | if (error) { | |
1332 | derr << "Could not load registry disk info for: " | |
1333 | << devpath << ". Error: " << error << dendl; | |
1334 | return error; | |
1335 | } | |
1336 | ||
1337 | WNBD_CONNECTION_INFO conn_info = { 0 }; | |
1338 | // If the device is currently disconnected but there is a persistent | |
1339 | // mapping record, we'll show that. | |
1340 | DWORD ret = WnbdShow(devpath.c_str(), &conn_info); | |
1341 | if (ret && ret != ERROR_FILE_NOT_FOUND) { | |
1342 | return -EINVAL; | |
1343 | } | |
1344 | ||
1345 | auto conn_props = conn_info.Properties; | |
1346 | cfg.active = conn_info.DiskNumber > 0 && is_process_running(conn_props.Pid); | |
1347 | f->open_object_section("device"); | |
1348 | f->dump_int("id", conn_props.Pid ? conn_props.Pid : -1); | |
1349 | f->dump_string("device", cfg.devpath); | |
1350 | f->dump_string("pool", cfg.poolname); | |
1351 | f->dump_string("namespace", cfg.nsname); | |
1352 | f->dump_string("image", cfg.imgname); | |
1353 | f->dump_string("snap", cfg.snapname); | |
1354 | f->dump_int("persistent", cfg.persistent); | |
1355 | f->dump_int("disk_number", conn_info.DiskNumber ? conn_info.DiskNumber : -1); | |
1356 | f->dump_string("status", cfg.active ? WNBD_STATUS_ACTIVE : WNBD_STATUS_INACTIVE); | |
1357 | f->dump_string("pnp_device_id", to_string(conn_info.PNPDeviceID)); | |
1358 | f->dump_int("readonly", conn_props.Flags.ReadOnly); | |
1359 | f->dump_int("block_size", conn_props.BlockSize); | |
1360 | f->dump_int("block_count", conn_props.BlockCount); | |
1361 | f->dump_int("flush_enabled", conn_props.Flags.FlushSupported); | |
1362 | f->close_section(); | |
1363 | f->flush(std::cout); | |
1364 | ||
1365 | return 0; | |
1366 | } | |
1367 | ||
1368 | static int do_stats(std::string search_devpath) | |
1369 | { | |
1370 | Config cfg; | |
1371 | WNBDDiskIterator wnbd_disk_iterator; | |
1372 | ||
1373 | while (wnbd_disk_iterator.get(&cfg)) { | |
1374 | if (cfg.devpath != search_devpath) | |
1375 | continue; | |
1376 | ||
1377 | AdminSocketClient client = AdminSocketClient(cfg.admin_sock_path); | |
1378 | std::string output; | |
1379 | std::string result = client.do_request("{\"prefix\":\"wnbd stats\"}", | |
1380 | &output); | |
1381 | if (!result.empty()) { | |
1382 | std::cerr << "Admin socket error: " << result << std::endl; | |
1383 | return -EINVAL; | |
1384 | } | |
1385 | ||
1386 | std::cout << output << std::endl; | |
1387 | return 0; | |
1388 | } | |
1389 | int error = wnbd_disk_iterator.get_error(); | |
1390 | if (!error) { | |
1391 | error = ENOENT; | |
1392 | } | |
1393 | ||
1394 | derr << "Could not find the specified disk." << dendl; | |
1395 | return -error; | |
1396 | } | |
1397 | ||
1398 | static int parse_args(std::vector<const char*>& args, | |
1399 | std::ostream *err_msg, | |
1400 | Command *command, Config *cfg) | |
1401 | { | |
1402 | std::string conf_file_list; | |
1403 | std::string cluster; | |
1404 | CephInitParameters iparams = ceph_argparse_early_args( | |
1405 | args, CEPH_ENTITY_TYPE_CLIENT, &cluster, &conf_file_list); | |
1406 | ||
1407 | ConfigProxy config{false}; | |
1408 | config->name = iparams.name; | |
1409 | config->cluster = cluster; | |
1410 | ||
1411 | if (!conf_file_list.empty()) { | |
1412 | config.parse_config_files(conf_file_list.c_str(), nullptr, 0); | |
1413 | } else { | |
1414 | config.parse_config_files(nullptr, nullptr, 0); | |
1415 | } | |
1416 | config.parse_env(CEPH_ENTITY_TYPE_CLIENT); | |
1417 | config.parse_argv(args); | |
1418 | cfg->poolname = config.get_val<std::string>("rbd_default_pool"); | |
1419 | ||
1420 | std::vector<const char*>::iterator i; | |
1421 | std::ostringstream err; | |
1422 | ||
1423 | // TODO: consider using boost::program_options like Device.cc does. | |
1424 | // This should simplify argument parsing. Also, some arguments must be tied | |
1425 | // to specific commands, for example the disconnect timeout. Luckily, | |
1426 | // this is enforced by the "rbd device" wrapper. | |
1427 | for (i = args.begin(); i != args.end(); ) { | |
1428 | if (ceph_argparse_flag(args, i, "-h", "--help", (char*)NULL)) { | |
1429 | return HELP_INFO; | |
1430 | } else if (ceph_argparse_flag(args, i, "-v", "--version", (char*)NULL)) { | |
1431 | return VERSION_INFO; | |
1432 | } else if (ceph_argparse_witharg(args, i, &cfg->devpath, "--device", (char *)NULL)) { | |
1433 | } else if (ceph_argparse_witharg(args, i, &cfg->format, err, "--format", | |
1434 | (char *)NULL)) { | |
1435 | } else if (ceph_argparse_flag(args, i, "--read-only", (char *)NULL)) { | |
1436 | cfg->readonly = true; | |
1437 | } else if (ceph_argparse_flag(args, i, "--exclusive", (char *)NULL)) { | |
1438 | cfg->exclusive = true; | |
1439 | } else if (ceph_argparse_flag(args, i, "--non-persistent", (char *)NULL)) { | |
1440 | cfg->persistent = false; | |
1441 | } else if (ceph_argparse_flag(args, i, "--pretty-format", (char *)NULL)) { | |
1442 | cfg->pretty_format = true; | |
1443 | } else if (ceph_argparse_flag(args, i, "--remap-failure-fatal", (char *)NULL)) { | |
1444 | cfg->remap_failure_fatal = true; | |
1445 | } else if (ceph_argparse_witharg(args, i, &cfg->parent_pipe, err, | |
1446 | "--pipe-name", (char *)NULL)) { | |
1447 | if (!err.str().empty()) { | |
1448 | *err_msg << "rbd-wnbd: " << err.str(); | |
1449 | return -EINVAL; | |
1450 | } | |
1451 | } else if (ceph_argparse_witharg(args, i, (int*)&cfg->wnbd_log_level, | |
1452 | err, "--wnbd-log-level", (char *)NULL)) { | |
1453 | if (!err.str().empty()) { | |
1454 | *err_msg << "rbd-wnbd: " << err.str(); | |
1455 | return -EINVAL; | |
1456 | } | |
1457 | if (cfg->wnbd_log_level < 0) { | |
1458 | *err_msg << "rbd-wnbd: Invalid argument for wnbd-log-level"; | |
1459 | return -EINVAL; | |
1460 | } | |
1461 | } else if (ceph_argparse_witharg(args, i, (int*)&cfg->io_req_workers, | |
1462 | err, "--io-req-workers", (char *)NULL)) { | |
1463 | if (!err.str().empty()) { | |
1464 | *err_msg << "rbd-wnbd: " << err.str(); | |
1465 | return -EINVAL; | |
1466 | } | |
1467 | if (cfg->io_req_workers <= 0) { | |
1468 | *err_msg << "rbd-wnbd: Invalid argument for io-req-workers"; | |
1469 | return -EINVAL; | |
1470 | } | |
1471 | } else if (ceph_argparse_witharg(args, i, (int*)&cfg->io_reply_workers, | |
1472 | err, "--io-reply-workers", (char *)NULL)) { | |
1473 | if (!err.str().empty()) { | |
1474 | *err_msg << "rbd-wnbd: " << err.str(); | |
1475 | return -EINVAL; | |
1476 | } | |
1477 | if (cfg->io_reply_workers <= 0) { | |
1478 | *err_msg << "rbd-wnbd: Invalid argument for io-reply-workers"; | |
1479 | return -EINVAL; | |
1480 | } | |
1481 | } else if (ceph_argparse_witharg(args, i, (int*)&cfg->service_thread_count, | |
1482 | err, "--service-thread-count", (char *)NULL)) { | |
1483 | if (!err.str().empty()) { | |
1484 | *err_msg << "rbd-wnbd: " << err.str(); | |
1485 | return -EINVAL; | |
1486 | } | |
1487 | if (cfg->service_thread_count <= 0) { | |
1488 | *err_msg << "rbd-wnbd: Invalid argument for service-thread-count"; | |
1489 | return -EINVAL; | |
1490 | } | |
1491 | } else if (ceph_argparse_flag(args, i, "--hard-disconnect", (char *)NULL)) { | |
1492 | cfg->hard_disconnect = true; | |
1493 | } else if (ceph_argparse_flag(args, i, | |
1494 | "--no-hard-disconnect-fallback", (char *)NULL)) { | |
1495 | cfg->hard_disconnect_fallback = false; | |
1496 | } else if (ceph_argparse_witharg(args, i, | |
1497 | (int*)&cfg->soft_disconnect_timeout, | |
1498 | err, "--soft-disconnect-timeout", | |
1499 | (char *)NULL)) { | |
1500 | if (!err.str().empty()) { | |
1501 | *err_msg << "rbd-wnbd: " << err.str(); | |
1502 | return -EINVAL; | |
1503 | } | |
1504 | if (cfg->soft_disconnect_timeout < 0) { | |
1505 | *err_msg << "rbd-wnbd: Invalid argument for soft-disconnect-timeout"; | |
1506 | return -EINVAL; | |
1507 | } | |
1508 | } else if (ceph_argparse_witharg(args, i, | |
1509 | (int*)&cfg->service_start_timeout, | |
1510 | err, "--start-timeout", | |
1511 | (char *)NULL)) { | |
1512 | if (!err.str().empty()) { | |
1513 | *err_msg << "rbd-wnbd: " << err.str(); | |
1514 | return -EINVAL; | |
1515 | } | |
1516 | if (cfg->service_start_timeout <= 0) { | |
1517 | *err_msg << "rbd-wnbd: Invalid argument for start-timeout"; | |
1518 | return -EINVAL; | |
1519 | } | |
1520 | } else if (ceph_argparse_witharg(args, i, | |
1521 | (int*)&cfg->image_map_timeout, | |
1522 | err, "--map-timeout", | |
1523 | (char *)NULL)) { | |
1524 | if (!err.str().empty()) { | |
1525 | *err_msg << "rbd-wnbd: " << err.str(); | |
1526 | return -EINVAL; | |
1527 | } | |
1528 | if (cfg->image_map_timeout <= 0) { | |
1529 | *err_msg << "rbd-wnbd: Invalid argument for map-timeout"; | |
1530 | return -EINVAL; | |
1531 | } | |
1532 | } else { | |
1533 | ++i; | |
1534 | } | |
1535 | } | |
1536 | ||
1537 | Command cmd = None; | |
1538 | if (args.begin() != args.end()) { | |
1539 | if (strcmp(*args.begin(), "map") == 0) { | |
1540 | cmd = Connect; | |
1541 | } else if (strcmp(*args.begin(), "unmap") == 0) { | |
1542 | cmd = Disconnect; | |
1543 | } else if (strcmp(*args.begin(), "list") == 0) { | |
1544 | cmd = List; | |
1545 | } else if (strcmp(*args.begin(), "show") == 0) { | |
1546 | cmd = Show; | |
1547 | } else if (strcmp(*args.begin(), "service") == 0) { | |
1548 | cmd = Service; | |
1549 | } else if (strcmp(*args.begin(), "stats") == 0) { | |
1550 | cmd = Stats; | |
1551 | } else if (strcmp(*args.begin(), "help") == 0) { | |
1552 | return HELP_INFO; | |
1553 | } else { | |
1554 | *err_msg << "rbd-wnbd: unknown command: " << *args.begin(); | |
1555 | return -EINVAL; | |
1556 | } | |
1557 | args.erase(args.begin()); | |
1558 | } | |
1559 | ||
1560 | if (cmd == None) { | |
1561 | *err_msg << "rbd-wnbd: must specify command"; | |
1562 | return -EINVAL; | |
1563 | } | |
1564 | ||
1565 | switch (cmd) { | |
1566 | case Connect: | |
1567 | case Disconnect: | |
1568 | case Show: | |
1569 | case Stats: | |
1570 | if (args.begin() == args.end()) { | |
1571 | *err_msg << "rbd-wnbd: must specify wnbd device or image-or-snap-spec"; | |
1572 | return -EINVAL; | |
1573 | } | |
1574 | if (parse_imgpath(*args.begin(), cfg, err_msg) < 0) { | |
1575 | return -EINVAL; | |
1576 | } | |
1577 | args.erase(args.begin()); | |
1578 | break; | |
1579 | default: | |
1580 | //shut up gcc; | |
1581 | break; | |
1582 | } | |
1583 | ||
1584 | if (args.begin() != args.end()) { | |
1585 | *err_msg << "rbd-wnbd: unknown args: " << *args.begin(); | |
1586 | return -EINVAL; | |
1587 | } | |
1588 | ||
1589 | *command = cmd; | |
1590 | return 0; | |
1591 | } | |
1592 | ||
1593 | static int rbd_wnbd(int argc, const char *argv[]) | |
1594 | { | |
f67539c2 | 1595 | Config cfg; |
20effc67 | 1596 | auto args = argv_to_vec(argc, argv); |
f67539c2 TL |
1597 | |
1598 | // Avoid using dout before calling "do_global_init" | |
1599 | if (args.empty()) { | |
1600 | std::cout << argv[0] << ": -h or --help for usage" << std::endl; | |
1601 | exit(1); | |
1602 | } | |
1603 | ||
1604 | std::ostringstream err_msg; | |
20effc67 | 1605 | int r = parse_args(args, &err_msg, &cmd, &cfg); |
f67539c2 TL |
1606 | if (r == HELP_INFO) { |
1607 | usage(); | |
1608 | return 0; | |
1609 | } else if (r == VERSION_INFO) { | |
1610 | std::cout << pretty_version_to_str() << std::endl; | |
1611 | return 0; | |
1612 | } else if (r < 0) { | |
1613 | std::cout << err_msg.str() << std::endl; | |
1614 | return r; | |
1615 | } | |
1616 | ||
1617 | auto cct = do_global_init(argc, argv, &cfg); | |
1618 | ||
1619 | WnbdSetLogger(WnbdHandler::LogMessage); | |
1620 | WnbdSetLogLevel(cfg.wnbd_log_level); | |
1621 | ||
1622 | switch (cmd) { | |
1623 | case Connect: | |
1624 | if (construct_devpath_if_missing(&cfg)) { | |
1625 | return -EINVAL; | |
1626 | } | |
1627 | r = do_map(&cfg); | |
1628 | if (r < 0) | |
1629 | return r; | |
1630 | break; | |
1631 | case Disconnect: | |
1632 | if (construct_devpath_if_missing(&cfg)) { | |
1633 | return -EINVAL; | |
1634 | } | |
1635 | r = do_unmap(&cfg, true); | |
1636 | if (r < 0) | |
1637 | return r; | |
1638 | break; | |
1639 | case List: | |
1640 | r = do_list_mapped_devices(cfg.format, cfg.pretty_format); | |
1641 | if (r < 0) | |
1642 | return r; | |
1643 | break; | |
1644 | case Show: | |
1645 | if (construct_devpath_if_missing(&cfg)) { | |
1646 | return r; | |
1647 | } | |
1648 | r = do_show_mapped_device(cfg.format, cfg.pretty_format, cfg.devpath); | |
1649 | if (r < 0) | |
1650 | return r; | |
1651 | break; | |
1652 | case Service: | |
1653 | { | |
1654 | RBDService service(cfg.hard_disconnect, cfg.soft_disconnect_timeout, | |
1655 | cfg.service_thread_count, | |
1656 | cfg.service_start_timeout, | |
1657 | cfg.image_map_timeout, | |
1658 | cfg.remap_failure_fatal); | |
1659 | // This call will block until the service stops. | |
1660 | r = RBDService::initialize(&service); | |
1661 | if (r < 0) | |
1662 | return r; | |
1663 | break; | |
1664 | } | |
1665 | case Stats: | |
1666 | if (construct_devpath_if_missing(&cfg)) { | |
1667 | return -EINVAL; | |
1668 | } | |
1669 | return do_stats(cfg.devpath); | |
1670 | default: | |
1671 | usage(); | |
1672 | break; | |
1673 | } | |
1674 | ||
1675 | return 0; | |
1676 | } | |
1677 | ||
1678 | int main(int argc, const char *argv[]) | |
1679 | { | |
1680 | SetConsoleCtrlHandler(console_handler_routine, true); | |
1681 | // Avoid the Windows Error Reporting dialog. | |
1682 | SetErrorMode(GetErrorMode() | SEM_NOGPFAULTERRORBOX); | |
1683 | int r = rbd_wnbd(argc, argv); | |
1684 | if (r < 0) { | |
1685 | return r; | |
1686 | } | |
1687 | return 0; | |
1688 | } |