]>
Commit | Line | Data |
---|---|---|
f85f8ebb | 1 | /* |
19b276cb | 2 | * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2016, 2017 Nicira, Inc. |
f85f8ebb BP |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | * you may not use this file except in compliance with the License. | |
6 | * You may obtain a copy of the License at: | |
7 | * | |
8 | * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | * | |
10 | * Unless required by applicable law or agreed to in writing, software | |
11 | * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | * See the License for the specific language governing permissions and | |
14 | * limitations under the License. | |
15 | */ | |
16 | ||
17 | #include <config.h> | |
18 | #include <errno.h> | |
19 | #include <fcntl.h> | |
20 | #include <getopt.h> | |
21 | #include <signal.h> | |
22 | #include <stdlib.h> | |
23 | #include <string.h> | |
24 | ||
4e92542c | 25 | #include "column.h" |
f85f8ebb BP |
26 | #include "command-line.h" |
27 | #include "compiler.h" | |
e4476f74 | 28 | #include "dirs.h" |
3e8a2ad1 | 29 | #include "openvswitch/dynamic-string.h" |
8a777cf6 | 30 | #include "fatal-signal.h" |
bd06962a | 31 | #include "file.h" |
1b1d2e6d | 32 | #include "hash.h" |
1e19e50e | 33 | #include "lockfile.h" |
41709ccc | 34 | #include "log.h" |
1b1d2e6d | 35 | #include "openvswitch/hmap.h" |
ee89ea7b | 36 | #include "openvswitch/json.h" |
f85f8ebb | 37 | #include "ovsdb.h" |
4e92542c | 38 | #include "ovsdb-data.h" |
f85f8ebb | 39 | #include "ovsdb-error.h" |
1b1d2e6d BP |
40 | #include "ovsdb-parser.h" |
41 | #include "raft.h" | |
42 | #include "raft-private.h" | |
43 | #include "smap.h" | |
1e19e50e | 44 | #include "socket-util.h" |
1b1d2e6d | 45 | #include "storage.h" |
f85f8ebb BP |
46 | #include "table.h" |
47 | #include "timeval.h" | |
1b1d2e6d | 48 | #include "transaction.h" |
f85f8ebb | 49 | #include "util.h" |
e6211adc | 50 | #include "openvswitch/vlog.h" |
5136ce49 | 51 | |
c6782bb0 BP |
52 | /* -m, --more: Verbosity level for "show-log" command output. */ |
53 | static int show_log_verbosity; | |
54 | ||
d6db7b3c LR |
55 | /* --role: RBAC role to use for "transact" and "query" commands. */ |
56 | static const char *rbac_role; | |
57 | ||
1b1d2e6d BP |
58 | /* --cid: Cluster ID for "join-cluster" command. */ |
59 | static struct uuid cid; | |
60 | ||
5f383751 | 61 | static const struct ovs_cmdl_command *get_all_commands(void); |
f85f8ebb | 62 | |
cab50449 | 63 | OVS_NO_RETURN static void usage(void); |
f85f8ebb BP |
64 | static void parse_options(int argc, char *argv[]); |
65 | ||
e4476f74 BP |
66 | static const char *default_db(void); |
67 | static const char *default_schema(void); | |
68 | ||
f85f8ebb BP |
69 | int |
70 | main(int argc, char *argv[]) | |
71 | { | |
1636c761 | 72 | struct ovs_cmdl_context ctx = { .argc = 0, }; |
f85f8ebb | 73 | set_program_name(argv[0]); |
f85f8ebb | 74 | parse_options(argc, argv); |
8a777cf6 | 75 | fatal_ignore_sigpipe(); |
1b1d2e6d | 76 | fatal_signal_init(); |
1636c761 RB |
77 | ctx.argc = argc - optind; |
78 | ctx.argv = argv + optind; | |
79 | ovs_cmdl_run_command(&ctx, get_all_commands()); | |
f85f8ebb BP |
80 | return 0; |
81 | } | |
82 | ||
83 | static void | |
84 | parse_options(int argc, char *argv[]) | |
85 | { | |
d6db7b3c | 86 | enum { |
1b1d2e6d BP |
87 | OPT_RBAC_ROLE = UCHAR_MAX + 1, |
88 | OPT_CID | |
d6db7b3c | 89 | }; |
07fc4ed3 | 90 | static const struct option long_options[] = { |
e3c17733 | 91 | {"more", no_argument, NULL, 'm'}, |
d6db7b3c | 92 | {"rbac-role", required_argument, NULL, OPT_RBAC_ROLE}, |
1b1d2e6d | 93 | {"cid", required_argument, NULL, OPT_CID}, |
e3c17733 BP |
94 | {"verbose", optional_argument, NULL, 'v'}, |
95 | {"help", no_argument, NULL, 'h'}, | |
66fa2c88 | 96 | {"option", no_argument, NULL, 'o'}, |
e3c17733 BP |
97 | {"version", no_argument, NULL, 'V'}, |
98 | {NULL, 0, NULL, 0}, | |
f85f8ebb | 99 | }; |
5f383751 | 100 | char *short_options = ovs_cmdl_long_options_to_short_options(long_options); |
f85f8ebb BP |
101 | |
102 | for (;;) { | |
103 | int c; | |
104 | ||
105 | c = getopt_long(argc, argv, short_options, long_options, NULL); | |
106 | if (c == -1) { | |
107 | break; | |
108 | } | |
109 | ||
110 | switch (c) { | |
c6782bb0 BP |
111 | case 'm': |
112 | show_log_verbosity++; | |
113 | break; | |
114 | ||
d6db7b3c LR |
115 | case OPT_RBAC_ROLE: |
116 | rbac_role = optarg; | |
117 | break; | |
118 | ||
1b1d2e6d BP |
119 | case OPT_CID: |
120 | if (!uuid_from_string(&cid, optarg) || uuid_is_zero(&cid)) { | |
121 | ovs_fatal(0, "%s: not a valid UUID", optarg); | |
122 | } | |
123 | break; | |
124 | ||
f85f8ebb BP |
125 | case 'h': |
126 | usage(); | |
127 | ||
66fa2c88 | 128 | case 'o': |
5f383751 | 129 | ovs_cmdl_print_options(long_options); |
66fa2c88 AW |
130 | exit(EXIT_SUCCESS); |
131 | ||
f85f8ebb | 132 | case 'V': |
55d5bb44 | 133 | ovs_print_version(0, 0); |
f85f8ebb BP |
134 | exit(EXIT_SUCCESS); |
135 | ||
136 | case 'v': | |
137 | vlog_set_verbosity(optarg); | |
138 | break; | |
139 | ||
140 | case '?': | |
141 | exit(EXIT_FAILURE); | |
142 | ||
143 | default: | |
144 | abort(); | |
145 | } | |
146 | } | |
147 | free(short_options); | |
148 | } | |
149 | ||
150 | static void | |
151 | usage(void) | |
152 | { | |
153 | printf("%s: Open vSwitch database management utility\n" | |
154 | "usage: %s [OPTIONS] COMMAND [ARG...]\n" | |
e4476f74 | 155 | " create [DB [SCHEMA]] create DB with the given SCHEMA\n" |
1b1d2e6d BP |
156 | " create-cluster DB CONTENTS LOCAL\n" |
157 | " create clustered DB with given CONTENTS and LOCAL address\n" | |
158 | " [--cid=UUID] join-cluster DB NAME LOCAL REMOTE...\n" | |
159 | " join clustered DB with given NAME and LOCAL and REMOTE addrs\n" | |
e4476f74 BP |
160 | " compact [DB [DST]] compact DB in-place (or to DST)\n" |
161 | " convert [DB [SCHEMA [DST]]] convert DB to SCHEMA (to DST)\n" | |
439902cb | 162 | " db-name [DB] report name of schema used by DB\n" |
e4476f74 BP |
163 | " db-version [DB] report version of schema used by DB\n" |
164 | " db-cksum [DB] report checksum of schema used by DB\n" | |
1b1d2e6d BP |
165 | " db-cid DB report cluster ID of clustered DB\n" |
166 | " db-sid DB report server ID of clustered DB\n" | |
167 | " db-local-address DB report local address of clustered DB\n" | |
168 | " db-is-clustered DB test whether DB is clustered\n" | |
169 | " db-is-standalone DB test whether DB is standalone\n" | |
439902cb | 170 | " schema-name [SCHEMA] report SCHEMA's name\n" |
e4476f74 BP |
171 | " schema-version [SCHEMA] report SCHEMA's schema version\n" |
172 | " schema-cksum [SCHEMA] report SCHEMA's checksum\n" | |
1b1d2e6d | 173 | " compare-versions A OP B compare OVSDB schema version numbers\n" |
e4476f74 BP |
174 | " query [DB] TRNS execute read-only transaction on DB\n" |
175 | " transact [DB] TRNS execute read/write transaction on DB\n" | |
176 | " [-m]... show-log [DB] print DB's log entries\n" | |
177 | "The default DB is %s.\n" | |
178 | "The default SCHEMA is %s.\n", | |
179 | program_name, program_name, default_db(), default_schema()); | |
f85f8ebb | 180 | vlog_usage(); |
d6db7b3c LR |
181 | printf("\ |
182 | \nOther options:\n\ | |
183 | -m, --more increase show-log verbosity\n\ | |
184 | --rbac-role=ROLE RBAC role for transact and query commands\n\ | |
185 | -h, --help display this help message\n\ | |
186 | -V, --version display version information\n"); | |
f85f8ebb BP |
187 | exit(EXIT_SUCCESS); |
188 | } | |
e4476f74 BP |
189 | |
190 | static const char * | |
191 | default_db(void) | |
192 | { | |
193 | static char *db; | |
194 | if (!db) { | |
f973f2af | 195 | db = xasprintf("%s/conf.db", ovs_dbdir()); |
e4476f74 BP |
196 | } |
197 | return db; | |
198 | } | |
199 | ||
200 | static const char * | |
201 | default_schema(void) | |
202 | { | |
203 | static char *schema; | |
204 | if (!schema) { | |
205 | schema = xasprintf("%s/vswitch.ovsschema", ovs_pkgdatadir()); | |
206 | } | |
207 | return schema; | |
208 | } | |
f85f8ebb BP |
209 | \f |
210 | static struct json * | |
211 | parse_json(const char *s) | |
212 | { | |
213 | struct json *json = json_from_string(s); | |
214 | if (json->type == JSON_STRING) { | |
fa37affa | 215 | ovs_fatal(0, "\"%s\": %s", s, json->string); |
f85f8ebb BP |
216 | } |
217 | return json; | |
218 | } | |
219 | ||
220 | static void | |
221 | print_and_free_json(struct json *json) | |
222 | { | |
223 | char *string = json_to_string(json, JSSF_SORT); | |
224 | json_destroy(json); | |
225 | puts(string); | |
226 | free(string); | |
227 | } | |
228 | ||
229 | static void | |
230 | check_ovsdb_error(struct ovsdb_error *error) | |
231 | { | |
232 | if (error) { | |
233 | ovs_fatal(0, "%s", ovsdb_error_to_string(error)); | |
234 | } | |
235 | } | |
1b1d2e6d BP |
236 | |
237 | /* Opens the standalone database 'filename' and returns its schema. */ | |
238 | static struct ovsdb_schema * | |
239 | read_standalone_schema(const char *filename) | |
240 | { | |
241 | struct ovsdb_storage *storage = ovsdb_storage_open_standalone(filename, | |
242 | false); | |
243 | struct ovsdb_schema *schema = ovsdb_storage_read_schema(storage); | |
244 | ovsdb_storage_close(storage); | |
245 | return schema; | |
246 | } | |
f85f8ebb BP |
247 | \f |
248 | static void | |
1636c761 | 249 | do_create(struct ovs_cmdl_context *ctx) |
f85f8ebb | 250 | { |
1636c761 RB |
251 | const char *db_file_name = ctx->argc >= 2 ? ctx->argv[1] : default_db(); |
252 | const char *schema_file_name = ctx->argc >= 3 ? ctx->argv[2] : default_schema(); | |
f85f8ebb | 253 | struct ovsdb_schema *schema; |
41709ccc | 254 | struct ovsdb_log *log; |
f85f8ebb BP |
255 | struct json *json; |
256 | ||
257 | /* Read schema from file and convert to JSON. */ | |
258 | check_ovsdb_error(ovsdb_schema_from_file(schema_file_name, &schema)); | |
259 | json = ovsdb_schema_to_json(schema); | |
90cc4071 | 260 | ovsdb_schema_destroy(schema); |
f85f8ebb BP |
261 | |
262 | /* Create database file. */ | |
19b276cb | 263 | check_ovsdb_error(ovsdb_log_open(db_file_name, OVSDB_MAGIC, |
1e0b7e94 | 264 | OVSDB_LOG_CREATE_EXCL, -1, &log)); |
1d0dead5 | 265 | check_ovsdb_error(ovsdb_log_write_and_free(log, json)); |
f70b61d3 | 266 | check_ovsdb_error(ovsdb_log_commit_block(log)); |
41709ccc | 267 | ovsdb_log_close(log); |
1b1d2e6d | 268 | } |
f85f8ebb | 269 | |
1b1d2e6d BP |
270 | static void |
271 | do_create_cluster(struct ovs_cmdl_context *ctx) | |
272 | { | |
273 | const char *db_file_name = ctx->argv[1]; | |
274 | const char *src_file_name = ctx->argv[2]; | |
275 | const char *local = ctx->argv[3]; | |
276 | ||
277 | struct ovsdb_schema *schema; | |
278 | struct json *data; | |
279 | ||
280 | struct ovsdb_error *error = ovsdb_schema_from_file(src_file_name, &schema); | |
281 | if (!error) { | |
282 | /* It's just a schema file. */ | |
283 | data = json_object_create(); | |
284 | } else { | |
285 | /* Not a schema file. Try reading it as a standalone database. */ | |
286 | ovsdb_error_destroy(error); | |
287 | ||
288 | struct ovsdb *ovsdb = ovsdb_file_read(src_file_name, false); | |
289 | char *comment = xasprintf("created from %s", src_file_name); | |
290 | data = ovsdb_to_txn_json(ovsdb, comment); | |
291 | free(comment); | |
292 | schema = ovsdb_schema_clone(ovsdb->schema); | |
293 | ovsdb_destroy(ovsdb); | |
294 | } | |
295 | ||
296 | ovsdb_schema_persist_ephemeral_columns(schema, src_file_name); | |
297 | ||
298 | struct json *schema_json = ovsdb_schema_to_json(schema); | |
299 | ||
300 | /* Create database file. */ | |
301 | struct json *snapshot = json_array_create_2(schema_json, data); | |
302 | check_ovsdb_error(raft_create_cluster(db_file_name, schema->name, | |
303 | local, snapshot)); | |
304 | ovsdb_schema_destroy(schema); | |
305 | json_destroy(snapshot); | |
f85f8ebb BP |
306 | } |
307 | ||
1b1d2e6d BP |
308 | static void |
309 | do_join_cluster(struct ovs_cmdl_context *ctx) | |
310 | { | |
311 | const char *db_file_name = ctx->argv[1]; | |
312 | const char *name = ctx->argv[2]; | |
313 | const char *local = ctx->argv[3]; | |
314 | ||
315 | /* Check for a plausible 'name'. */ | |
316 | if (!ovsdb_parser_is_id(name)) { | |
317 | ovs_fatal(0, "%s: not a valid schema name (use \"schema-name\" " | |
318 | "command to find the correct name)", name); | |
319 | } | |
320 | ||
321 | /* Create database file. */ | |
322 | struct sset remote_addrs = SSET_INITIALIZER(&remote_addrs); | |
323 | for (size_t i = 4; i < ctx->argc; i++) { | |
324 | sset_add(&remote_addrs, ctx->argv[i]); | |
325 | } | |
326 | check_ovsdb_error(raft_join_cluster(db_file_name, name, local, | |
327 | &remote_addrs, | |
328 | uuid_is_zero(&cid) ? NULL : &cid)); | |
329 | sset_destroy(&remote_addrs); | |
330 | } | |
331 | ||
332 | static struct ovsdb_error * | |
333 | write_standalone_db(const char *file_name, const char *comment, | |
334 | const struct ovsdb *db) | |
335 | { | |
336 | struct ovsdb_log *log; | |
337 | struct ovsdb_error *error = ovsdb_log_open(file_name, OVSDB_MAGIC, | |
338 | OVSDB_LOG_CREATE, false, &log); | |
339 | if (error) { | |
340 | return error; | |
341 | } | |
342 | ||
1d0dead5 | 343 | error = ovsdb_log_write_and_free(log, ovsdb_schema_to_json(db->schema)); |
1b1d2e6d | 344 | if (!error) { |
1d0dead5 | 345 | error = ovsdb_log_write_and_free(log, ovsdb_to_txn_json(db, comment)); |
1b1d2e6d BP |
346 | } |
347 | ovsdb_log_close(log); | |
348 | ||
349 | if (error) { | |
350 | remove(file_name); | |
351 | } | |
352 | return error; | |
353 | } | |
354 | ||
355 | /* Reads 'src_name' and writes it back, compacted, to 'dst_name', adding the | |
356 | * specified 'comment'. If 'new_schema' is nonull, converts the databse to | |
357 | * that schema. | |
358 | * | |
359 | * Standalone databases only. */ | |
1e19e50e | 360 | static void |
a35ae81c | 361 | compact_or_convert(const char *src_name_, const char *dst_name_, |
1b1d2e6d | 362 | struct ovsdb_schema *new_schema, const char *comment) |
1e19e50e | 363 | { |
a35ae81c | 364 | bool in_place = dst_name_ == NULL; |
1e19e50e | 365 | |
a35ae81c BP |
366 | /* Dereference symlinks for source and destination names. In the in-place |
367 | * case this ensures that, if the source name is a symlink, we replace its | |
368 | * target instead of replacing the symlink by a regular file. In the | |
369 | * non-in-place, this has the same effect for the destination name. */ | |
1b1d2e6d BP |
370 | char *src_name = follow_symlinks(src_name_); |
371 | char *dst_name = (in_place | |
372 | ? xasprintf("%s.tmp", src_name) | |
373 | : follow_symlinks(dst_name_)); | |
a35ae81c | 374 | |
aebfc869 | 375 | /* Lock the source, if we will be replacing it. */ |
1b1d2e6d | 376 | struct lockfile *src_lock = NULL; |
1e19e50e | 377 | if (in_place) { |
1b1d2e6d | 378 | int retval = lockfile_lock(src_name, &src_lock); |
aebfc869 BP |
379 | if (retval) { |
380 | ovs_fatal(retval, "%s: failed to lock lockfile", src_name); | |
381 | } | |
1e19e50e BP |
382 | } |
383 | ||
aebfc869 | 384 | /* Get (temporary) destination and lock it. */ |
1b1d2e6d BP |
385 | struct lockfile *dst_lock = NULL; |
386 | int retval = lockfile_lock(dst_name, &dst_lock); | |
1e19e50e BP |
387 | if (retval) { |
388 | ovs_fatal(retval, "%s: failed to lock lockfile", dst_name); | |
389 | } | |
390 | ||
391 | /* Save a copy. */ | |
1b1d2e6d BP |
392 | struct ovsdb *ovsdb = (new_schema |
393 | ? ovsdb_file_read_as_schema(src_name, new_schema) | |
394 | : ovsdb_file_read(src_name, false)); | |
395 | ovsdb_storage_close(ovsdb->storage); | |
396 | ovsdb->storage = NULL; | |
397 | check_ovsdb_error(write_standalone_db(dst_name, comment, ovsdb)); | |
398 | ovsdb_destroy(ovsdb); | |
1e19e50e BP |
399 | |
400 | /* Replace source. */ | |
401 | if (in_place) { | |
10c119c3 GS |
402 | #ifdef _WIN32 |
403 | unlink(src_name); | |
404 | #endif | |
1e19e50e BP |
405 | if (rename(dst_name, src_name)) { |
406 | ovs_fatal(errno, "failed to rename \"%s\" to \"%s\"", | |
407 | dst_name, src_name); | |
408 | } | |
409 | fsync_parent_dir(dst_name); | |
1e19e50e BP |
410 | lockfile_unlock(src_lock); |
411 | } | |
412 | ||
413 | lockfile_unlock(dst_lock); | |
e49190c4 | 414 | |
a35ae81c BP |
415 | free(src_name); |
416 | free(dst_name); | |
1e19e50e BP |
417 | } |
418 | ||
419 | static void | |
1636c761 | 420 | do_compact(struct ovs_cmdl_context *ctx) |
1e19e50e | 421 | { |
1636c761 RB |
422 | const char *db = ctx->argc >= 2 ? ctx->argv[1] : default_db(); |
423 | const char *target = ctx->argc >= 3 ? ctx->argv[2] : NULL; | |
e4476f74 | 424 | |
8a07709c | 425 | compact_or_convert(db, target, NULL, "compacted by ovsdb-tool "VERSION); |
1e19e50e BP |
426 | } |
427 | ||
428 | static void | |
1636c761 | 429 | do_convert(struct ovs_cmdl_context *ctx) |
1e19e50e | 430 | { |
1636c761 RB |
431 | const char *db = ctx->argc >= 2 ? ctx->argv[1] : default_db(); |
432 | const char *schema = ctx->argc >= 3 ? ctx->argv[2] : default_schema(); | |
433 | const char *target = ctx->argc >= 4 ? ctx->argv[3] : NULL; | |
1e19e50e BP |
434 | struct ovsdb_schema *new_schema; |
435 | ||
e4476f74 BP |
436 | check_ovsdb_error(ovsdb_schema_from_file(schema, &new_schema)); |
437 | compact_or_convert(db, target, new_schema, | |
8a07709c | 438 | "converted by ovsdb-tool "VERSION); |
1e19e50e BP |
439 | } |
440 | ||
403e3a25 | 441 | static void |
1636c761 | 442 | do_needs_conversion(struct ovs_cmdl_context *ctx) |
403e3a25 | 443 | { |
1636c761 RB |
444 | const char *db_file_name = ctx->argc >= 2 ? ctx->argv[1] : default_db(); |
445 | const char *schema_file_name = ctx->argc >= 3 ? ctx->argv[2] : default_schema(); | |
1b1d2e6d BP |
446 | struct ovsdb_schema *schema1 = read_standalone_schema(db_file_name); |
447 | struct ovsdb_schema *schema2; | |
403e3a25 | 448 | |
403e3a25 BP |
449 | check_ovsdb_error(ovsdb_schema_from_file(schema_file_name, &schema2)); |
450 | puts(ovsdb_schema_equal(schema1, schema2) ? "no" : "yes"); | |
451 | ovsdb_schema_destroy(schema1); | |
452 | ovsdb_schema_destroy(schema2); | |
453 | } | |
454 | ||
439902cb BP |
455 | static void |
456 | do_db_name(struct ovs_cmdl_context *ctx) | |
457 | { | |
458 | const char *db_file_name = ctx->argc >= 2 ? ctx->argv[1] : default_db(); | |
439902cb | 459 | |
1b1d2e6d BP |
460 | struct ovsdb_log *log; |
461 | check_ovsdb_error(ovsdb_log_open(db_file_name, OVSDB_MAGIC"|"RAFT_MAGIC, | |
462 | OVSDB_LOG_READ_ONLY, -1, &log)); | |
463 | if (!strcmp(ovsdb_log_get_magic(log), OVSDB_MAGIC)) { | |
464 | struct json *schema_json; | |
465 | check_ovsdb_error(ovsdb_log_read(log, &schema_json)); | |
466 | ||
467 | struct ovsdb_schema *schema; | |
468 | check_ovsdb_error(ovsdb_schema_from_json(schema_json, &schema)); | |
469 | ||
470 | puts(schema->name); | |
471 | ||
472 | ovsdb_schema_destroy(schema); | |
473 | json_destroy(schema_json); | |
474 | } else if (!strcmp(ovsdb_log_get_magic(log), RAFT_MAGIC)) { | |
475 | struct raft_metadata md; | |
476 | check_ovsdb_error(raft_read_metadata(log, &md)); | |
477 | puts(md.name); | |
478 | raft_metadata_destroy(&md); | |
479 | } else { | |
480 | OVS_NOT_REACHED(); | |
481 | } | |
482 | ||
483 | ovsdb_log_close(log); | |
439902cb BP |
484 | } |
485 | ||
8159b984 | 486 | static void |
1636c761 | 487 | do_db_version(struct ovs_cmdl_context *ctx) |
8159b984 | 488 | { |
1636c761 | 489 | const char *db_file_name = ctx->argc >= 2 ? ctx->argv[1] : default_db(); |
1b1d2e6d | 490 | struct ovsdb_schema *schema = read_standalone_schema(db_file_name); |
8159b984 | 491 | |
e1ebc8ce BP |
492 | puts(schema->version); |
493 | ovsdb_schema_destroy(schema); | |
8159b984 BP |
494 | } |
495 | ||
6aa09313 | 496 | static void |
1636c761 | 497 | do_db_cksum(struct ovs_cmdl_context *ctx) |
6aa09313 | 498 | { |
1636c761 | 499 | const char *db_file_name = ctx->argc >= 2 ? ctx->argv[1] : default_db(); |
1b1d2e6d | 500 | struct ovsdb_schema *schema = read_standalone_schema(db_file_name); |
6aa09313 BP |
501 | puts(schema->cksum); |
502 | ovsdb_schema_destroy(schema); | |
503 | } | |
504 | ||
1b1d2e6d BP |
505 | static struct raft_metadata |
506 | read_cluster_metadata(const char *filename) | |
507 | { | |
508 | struct ovsdb_log *log; | |
509 | check_ovsdb_error(ovsdb_log_open(filename, OVSDB_MAGIC"|"RAFT_MAGIC, | |
510 | OVSDB_LOG_READ_ONLY, -1, &log)); | |
511 | if (strcmp(ovsdb_log_get_magic(log), RAFT_MAGIC)) { | |
512 | ovs_fatal(0, "%s: not a clustered database", filename); | |
513 | } | |
514 | ||
515 | struct raft_metadata md; | |
516 | check_ovsdb_error(raft_read_metadata(log, &md)); | |
517 | ||
518 | ovsdb_log_close(log); | |
519 | ||
520 | return md; | |
521 | } | |
522 | ||
8159b984 | 523 | static void |
1b1d2e6d BP |
524 | do_db_cid(struct ovs_cmdl_context *ctx) |
525 | { | |
526 | const char *db_file_name = ctx->argv[1]; | |
527 | struct raft_metadata md = read_cluster_metadata(db_file_name); | |
528 | if (uuid_is_zero(&md.cid)) { | |
529 | fprintf(stderr, "%s: cluster ID not yet known\n", db_file_name); | |
530 | exit(2); | |
531 | } | |
532 | printf(UUID_FMT"\n", UUID_ARGS(&md.cid)); | |
533 | raft_metadata_destroy(&md); | |
534 | } | |
535 | ||
536 | static void | |
537 | do_db_sid(struct ovs_cmdl_context *ctx) | |
538 | { | |
539 | const char *db_file_name = ctx->argv[1]; | |
540 | struct raft_metadata md = read_cluster_metadata(db_file_name); | |
541 | printf(UUID_FMT"\n", UUID_ARGS(&md.sid)); | |
542 | raft_metadata_destroy(&md); | |
543 | } | |
544 | ||
545 | static void | |
546 | do_db_local_address(struct ovs_cmdl_context *ctx) | |
547 | { | |
548 | const char *db_file_name = ctx->argv[1]; | |
549 | struct raft_metadata md = read_cluster_metadata(db_file_name); | |
550 | puts(md.local); | |
551 | raft_metadata_destroy(&md); | |
552 | } | |
553 | ||
554 | static void | |
555 | do_db_has_magic(struct ovs_cmdl_context *ctx, const char *magic) | |
556 | { | |
557 | const char *filename = ctx->argv[1]; | |
558 | struct ovsdb_log *log; | |
559 | ||
560 | check_ovsdb_error(ovsdb_log_open(filename, OVSDB_MAGIC"|"RAFT_MAGIC, | |
561 | OVSDB_LOG_READ_ONLY, -1, &log)); | |
562 | if (strcmp(ovsdb_log_get_magic(log), magic)) { | |
563 | exit(2); | |
564 | } | |
565 | } | |
566 | ||
567 | static void | |
568 | do_db_is_clustered(struct ovs_cmdl_context *ctx) | |
569 | { | |
570 | do_db_has_magic(ctx, RAFT_MAGIC); | |
571 | } | |
572 | ||
573 | static void | |
574 | do_db_is_standalone(struct ovs_cmdl_context *ctx) | |
575 | { | |
576 | do_db_has_magic(ctx, OVSDB_MAGIC); | |
577 | } | |
578 | ||
579 | static void | |
580 | do_schema_name(struct ovs_cmdl_context *ctx) | |
8159b984 | 581 | { |
1636c761 | 582 | const char *schema_file_name = ctx->argc >= 2 ? ctx->argv[1] : default_schema(); |
8159b984 BP |
583 | struct ovsdb_schema *schema; |
584 | ||
585 | check_ovsdb_error(ovsdb_schema_from_file(schema_file_name, &schema)); | |
1b1d2e6d | 586 | puts(schema->name); |
8159b984 BP |
587 | ovsdb_schema_destroy(schema); |
588 | } | |
589 | ||
6aa09313 | 590 | static void |
1b1d2e6d | 591 | do_schema_version(struct ovs_cmdl_context *ctx) |
6aa09313 | 592 | { |
1636c761 | 593 | const char *schema_file_name = ctx->argc >= 2 ? ctx->argv[1] : default_schema(); |
6aa09313 BP |
594 | struct ovsdb_schema *schema; |
595 | ||
596 | check_ovsdb_error(ovsdb_schema_from_file(schema_file_name, &schema)); | |
1b1d2e6d | 597 | puts(schema->version); |
6aa09313 BP |
598 | ovsdb_schema_destroy(schema); |
599 | } | |
600 | ||
439902cb | 601 | static void |
1b1d2e6d | 602 | do_schema_cksum(struct ovs_cmdl_context *ctx) |
439902cb BP |
603 | { |
604 | const char *schema_file_name | |
605 | = ctx->argc >= 2 ? ctx->argv[1] : default_schema(); | |
606 | struct ovsdb_schema *schema; | |
607 | ||
608 | check_ovsdb_error(ovsdb_schema_from_file(schema_file_name, &schema)); | |
1b1d2e6d | 609 | puts(schema->cksum); |
439902cb BP |
610 | ovsdb_schema_destroy(schema); |
611 | } | |
612 | ||
1b1d2e6d | 613 | /* Standalone databases only. */ |
f85f8ebb | 614 | static void |
1b1d2e6d | 615 | transact(struct ovs_cmdl_context *ctx, bool rw) |
f85f8ebb | 616 | { |
1b1d2e6d BP |
617 | const char *db_file_name = ctx->argc >= 3 ? ctx->argv[1] : default_db(); |
618 | const char *transaction = ctx->argv[ctx->argc - 1]; | |
f85f8ebb | 619 | |
1b1d2e6d BP |
620 | struct ovsdb *ovsdb = ovsdb_file_read(db_file_name, rw); |
621 | struct json *request = parse_json(transaction); | |
622 | struct json *result = ovsdb_execute(ovsdb, NULL, request, false, | |
623 | rbac_role, NULL, 0, NULL); | |
f85f8ebb BP |
624 | json_destroy(request); |
625 | ||
626 | print_and_free_json(result); | |
1b1d2e6d | 627 | ovsdb_destroy(ovsdb); |
f85f8ebb BP |
628 | } |
629 | ||
630 | static void | |
1636c761 | 631 | do_query(struct ovs_cmdl_context *ctx) |
f85f8ebb | 632 | { |
1b1d2e6d | 633 | transact(ctx, false); |
f85f8ebb BP |
634 | } |
635 | ||
636 | static void | |
1636c761 | 637 | do_transact(struct ovs_cmdl_context *ctx) |
f85f8ebb | 638 | { |
1b1d2e6d | 639 | transact(ctx, true); |
f85f8ebb BP |
640 | } |
641 | ||
c6782bb0 | 642 | static void |
1b1d2e6d | 643 | print_db_changes(struct shash *tables, struct smap *names, |
4e92542c | 644 | const struct ovsdb_schema *schema) |
c6782bb0 BP |
645 | { |
646 | struct shash_node *n1; | |
647 | ||
1b1d2e6d | 648 | int i = 0; |
c6782bb0 BP |
649 | SHASH_FOR_EACH (n1, tables) { |
650 | const char *table = n1->name; | |
4e92542c | 651 | struct ovsdb_table_schema *table_schema; |
c6782bb0 BP |
652 | struct json *rows = n1->data; |
653 | struct shash_node *n2; | |
654 | ||
655 | if (n1->name[0] == '_' || rows->type != JSON_OBJECT) { | |
656 | continue; | |
657 | } | |
658 | ||
1b1d2e6d BP |
659 | if (i++ == 0) { |
660 | putchar('\n'); | |
661 | } | |
662 | ||
663 | table_schema = schema ? shash_find_data(&schema->tables, table) : NULL; | |
c6782bb0 BP |
664 | SHASH_FOR_EACH (n2, json_object(rows)) { |
665 | const char *row_uuid = n2->name; | |
666 | struct json *columns = n2->data; | |
667 | struct shash_node *n3; | |
c6782bb0 | 668 | |
1b1d2e6d BP |
669 | const char *old_name = smap_get(names, row_uuid); |
670 | char *new_name = NULL; | |
c6782bb0 BP |
671 | if (columns->type == JSON_OBJECT) { |
672 | struct json *new_name_json; | |
673 | ||
674 | new_name_json = shash_find_data(json_object(columns), "name"); | |
675 | if (new_name_json) { | |
676 | new_name = json_to_string(new_name_json, JSSF_SORT); | |
c6782bb0 BP |
677 | } |
678 | } | |
679 | ||
5a0e4aec | 680 | printf(" table %s", table); |
c6782bb0 BP |
681 | |
682 | if (!old_name) { | |
683 | if (new_name) { | |
a0a15a00 | 684 | printf(" insert row %s (%.8s):\n", new_name, row_uuid); |
c6782bb0 BP |
685 | } else { |
686 | printf(" insert row %.8s:\n", row_uuid); | |
687 | } | |
688 | } else { | |
a0a15a00 | 689 | printf(" row %s (%.8s):\n", old_name, row_uuid); |
c6782bb0 BP |
690 | } |
691 | ||
692 | if (columns->type == JSON_OBJECT) { | |
693 | if (show_log_verbosity > 1) { | |
694 | SHASH_FOR_EACH (n3, json_object(columns)) { | |
695 | const char *column = n3->name; | |
4e92542c | 696 | const struct ovsdb_column *column_schema; |
c6782bb0 | 697 | struct json *value = n3->data; |
4e92542c BP |
698 | char *value_string = NULL; |
699 | ||
700 | column_schema = | |
701 | (table_schema | |
702 | ? shash_find_data(&table_schema->columns, column) | |
703 | : NULL); | |
704 | if (column_schema) { | |
4e92542c | 705 | const struct ovsdb_type *type; |
a15fce8e | 706 | struct ovsdb_error *error; |
4e92542c BP |
707 | struct ovsdb_datum datum; |
708 | ||
709 | type = &column_schema->type; | |
710 | error = ovsdb_datum_from_json(&datum, type, | |
711 | value, NULL); | |
712 | if (!error) { | |
713 | struct ds s; | |
714 | ||
715 | ds_init(&s); | |
716 | ovsdb_datum_to_string(&datum, type, &s); | |
717 | value_string = ds_steal_cstr(&s); | |
a15fce8e BP |
718 | } else { |
719 | ovsdb_error_destroy(error); | |
4e92542c BP |
720 | } |
721 | } | |
722 | if (!value_string) { | |
723 | value_string = json_to_string(value, JSSF_SORT); | |
724 | } | |
5a0e4aec | 725 | printf(" %s=%s\n", column, value_string); |
c6782bb0 BP |
726 | free(value_string); |
727 | } | |
728 | } | |
1b1d2e6d BP |
729 | |
730 | if (new_name && (!old_name || strcmp(old_name, new_name))) { | |
731 | smap_replace_nocopy(names, row_uuid, new_name); | |
732 | new_name = NULL; | |
733 | } else if (!old_name) { | |
734 | smap_add_nocopy(names, xstrdup(row_uuid), | |
735 | xmemdup0(row_uuid, 8)); | |
c6782bb0 BP |
736 | } |
737 | } else if (columns->type == JSON_NULL) { | |
5a0e4aec | 738 | printf(" delete row\n"); |
1b1d2e6d | 739 | smap_remove(names, row_uuid); |
c6782bb0 BP |
740 | } |
741 | ||
1b1d2e6d | 742 | free(new_name); |
c6782bb0 BP |
743 | } |
744 | } | |
745 | } | |
746 | ||
722f6301 | 747 | static void |
1b1d2e6d BP |
748 | print_change_record(const struct json *json, const struct ovsdb_schema *schema, |
749 | struct smap *names) | |
722f6301 | 750 | { |
1b1d2e6d BP |
751 | if (!json || json->type != JSON_OBJECT) { |
752 | return; | |
753 | } | |
722f6301 | 754 | |
1b1d2e6d BP |
755 | struct json *date, *comment; |
756 | ||
757 | date = shash_find_data(json_object(json), "_date"); | |
758 | if (date && date->type == JSON_INTEGER) { | |
759 | long long int t = json_integer(date); | |
760 | char *s; | |
761 | ||
762 | if (t < INT32_MAX) { | |
763 | /* Older versions of ovsdb wrote timestamps in seconds. */ | |
764 | t *= 1000; | |
765 | } | |
766 | ||
767 | s = xastrftime_msec(" %Y-%m-%d %H:%M:%S.###", t, true); | |
768 | fputs(s, stdout); | |
769 | free(s); | |
770 | } | |
771 | ||
772 | comment = shash_find_data(json_object(json), "_comment"); | |
773 | if (comment && comment->type == JSON_STRING) { | |
774 | printf(" \"%s\"", json_string(comment)); | |
775 | } | |
776 | ||
777 | if (show_log_verbosity > 0) { | |
778 | print_db_changes(json_object(json), names, schema); | |
779 | } | |
780 | } | |
781 | ||
782 | static void | |
783 | do_show_log_standalone(struct ovsdb_log *log) | |
784 | { | |
785 | struct smap names = SMAP_INITIALIZER(&names); | |
786 | struct ovsdb_schema *schema = NULL; | |
787 | ||
788 | for (unsigned int i = 0; ; i++) { | |
722f6301 BP |
789 | struct json *json; |
790 | ||
791 | check_ovsdb_error(ovsdb_log_read(log, &json)); | |
792 | if (!json) { | |
793 | break; | |
794 | } | |
795 | ||
796 | printf("record %u:", i); | |
4e92542c BP |
797 | if (i == 0) { |
798 | check_ovsdb_error(ovsdb_schema_from_json(json, &schema)); | |
799 | printf(" \"%s\" schema, version=\"%s\", cksum=\"%s\"\n", | |
800 | schema->name, schema->version, schema->cksum); | |
1b1d2e6d BP |
801 | } else { |
802 | print_change_record(json, schema, &names); | |
803 | } | |
804 | json_destroy(json); | |
805 | putchar('\n'); | |
806 | } | |
807 | ||
808 | ovsdb_schema_destroy(schema); | |
809 | smap_destroy(&names); | |
810 | } | |
811 | ||
812 | static void | |
813 | print_servers(const char *name, const struct json *servers) | |
814 | { | |
815 | if (!servers) { | |
816 | return; | |
817 | } | |
722f6301 | 818 | |
1b1d2e6d | 819 | printf(" %s: ", name); |
1ff1065c | 820 | |
1b1d2e6d BP |
821 | const struct shash_node **nodes = shash_sort(json_object(servers)); |
822 | size_t n = shash_count(json_object(servers)); | |
823 | for (size_t i = 0; i < n; i++) { | |
824 | if (i > 0) { | |
825 | printf(", "); | |
826 | } | |
827 | ||
828 | const struct shash_node *node = nodes[i]; | |
829 | printf("%.4s(", node->name); | |
830 | ||
831 | const struct json *address = node->data; | |
832 | char *s = json_to_string(address, JSSF_SORT); | |
833 | fputs(s, stdout); | |
834 | free(s); | |
835 | ||
836 | putchar(')'); | |
837 | } | |
838 | free(nodes); | |
839 | putchar('\n'); | |
840 | } | |
841 | ||
842 | static void | |
843 | print_data(const char *prefix, const struct json *data, | |
844 | struct ovsdb_schema **schemap, struct smap *names) | |
845 | { | |
846 | if (!data) { | |
847 | return; | |
848 | } | |
849 | ||
850 | if (json_array(data)->n != 2) { | |
851 | printf(" ***invalid data***\n"); | |
852 | return; | |
853 | } | |
854 | ||
855 | const struct json *schema_json = json_array(data)->elems[0]; | |
856 | if (schema_json->type != JSON_NULL) { | |
857 | struct ovsdb_schema *schema; | |
858 | ||
859 | check_ovsdb_error(ovsdb_schema_from_json(schema_json, &schema)); | |
860 | printf(" %sschema: \"%s\", version=\"%s\", cksum=\"%s\"\n", | |
861 | prefix, schema->name, schema->version, schema->cksum); | |
862 | ||
863 | ovsdb_schema_destroy(*schemap); | |
864 | *schemap = schema; | |
865 | } | |
866 | ||
867 | print_change_record(json_array(data)->elems[1], *schemap, names); | |
868 | } | |
869 | ||
870 | static void | |
871 | print_raft_header(const struct raft_header *h, | |
872 | struct ovsdb_schema **schemap, struct smap *names) | |
873 | { | |
874 | printf(" name: \"%s\'\n", h->name); | |
875 | printf(" local address: \"%s\"\n", h->local_address); | |
876 | printf(" server_id: "SID_FMT"\n", SID_ARGS(&h->sid)); | |
877 | if (!uuid_is_zero(&h->cid)) { | |
878 | printf(" cluster_id: "CID_FMT"\n", CID_ARGS(&h->cid)); | |
879 | } | |
880 | if (!sset_is_empty(&h->remote_addresses)) { | |
881 | printf(" remote_addresses:"); | |
882 | ||
883 | const char *s; | |
884 | SSET_FOR_EACH (s, &h->remote_addresses) { | |
885 | printf(" %s", s); | |
886 | } | |
887 | putchar('\n'); | |
888 | } | |
889 | if (h->snap_index) { | |
890 | printf(" prev_index: %"PRIu64"\n", h->snap_index); | |
891 | printf(" prev_term: %"PRIu64"\n", h->snap.term); | |
892 | print_servers("prev_servers", h->snap.servers); | |
893 | if (!uuid_is_zero(&h->snap.eid)) { | |
894 | printf(" prev_eid: %04x\n", uuid_prefix(&h->snap.eid, 4)); | |
895 | } | |
896 | print_data("prev_", h->snap.data, schemap, names); | |
897 | } | |
898 | } | |
899 | ||
900 | static void | |
901 | print_raft_record(const struct raft_record *r, | |
902 | struct ovsdb_schema **schemap, struct smap *names) | |
903 | { | |
904 | if (r->comment) { | |
905 | printf(" comment: \"%s\"\n", r->comment); | |
906 | } | |
907 | if (r->term) { | |
908 | printf(" term: %"PRIu64"\n", r->term); | |
909 | } | |
910 | ||
911 | switch (r->type) { | |
912 | case RAFT_REC_ENTRY: | |
913 | printf(" index: %"PRIu64"\n", r->entry.index); | |
914 | print_servers("servers", r->entry.servers); | |
915 | if (!uuid_is_zero(&r->entry.eid)) { | |
916 | printf(" eid: %04x\n", uuid_prefix(&r->entry.eid, 4)); | |
917 | } | |
918 | print_data("", r->entry.data, schemap, names); | |
919 | break; | |
920 | ||
921 | case RAFT_REC_TERM: | |
922 | break; | |
923 | ||
924 | case RAFT_REC_VOTE: | |
925 | printf(" vote: "SID_FMT"\n", SID_ARGS(&r->sid)); | |
926 | break; | |
927 | ||
928 | case RAFT_REC_NOTE: | |
929 | printf(" note: \"%s\"\n", r->note); | |
930 | break; | |
931 | ||
932 | case RAFT_REC_COMMIT_INDEX: | |
933 | printf(" commit_index: %"PRIu64"\n", r->commit_index); | |
934 | break; | |
935 | ||
936 | case RAFT_REC_LEADER: | |
937 | printf(" leader: "SID_FMT"\n", SID_ARGS(&r->sid)); | |
938 | break; | |
939 | ||
940 | default: | |
941 | OVS_NOT_REACHED(); | |
942 | } | |
943 | } | |
944 | ||
945 | static void | |
946 | do_show_log_cluster(struct ovsdb_log *log) | |
947 | { | |
948 | struct smap names = SMAP_INITIALIZER(&names); | |
949 | struct ovsdb_schema *schema = NULL; | |
950 | for (unsigned int i = 0; ; i++) { | |
951 | struct json *json; | |
952 | check_ovsdb_error(ovsdb_log_read(log, &json)); | |
953 | if (!json) { | |
954 | break; | |
955 | } | |
956 | ||
957 | printf("record %u:\n", i); | |
958 | struct ovsdb_error *error; | |
959 | if (i == 0) { | |
960 | struct raft_header h; | |
961 | error = raft_header_from_json(&h, json); | |
962 | if (!error) { | |
963 | print_raft_header(&h, &schema, &names); | |
964 | raft_header_uninit(&h); | |
965 | } | |
966 | } else { | |
967 | struct raft_record r; | |
968 | error = raft_record_from_json(&r, json); | |
969 | if (!error) { | |
970 | print_raft_record(&r, &schema, &names); | |
971 | raft_record_uninit(&r); | |
972 | } | |
973 | } | |
974 | if (error) { | |
975 | char *s = ovsdb_error_to_string_free(error); | |
976 | puts(s); | |
977 | free(s); | |
978 | } | |
979 | ||
980 | putchar('\n'); | |
981 | } | |
982 | ||
983 | ovsdb_schema_destroy(schema); | |
984 | smap_destroy(&names); | |
985 | } | |
986 | ||
987 | static void | |
988 | do_show_log(struct ovs_cmdl_context *ctx) | |
989 | { | |
990 | const char *db_file_name = ctx->argc >= 2 ? ctx->argv[1] : default_db(); | |
991 | struct ovsdb_log *log; | |
992 | ||
993 | check_ovsdb_error(ovsdb_log_open(db_file_name, OVSDB_MAGIC"|"RAFT_MAGIC, | |
994 | OVSDB_LOG_READ_ONLY, -1, &log)); | |
995 | if (!strcmp(ovsdb_log_get_magic(log), OVSDB_MAGIC)) { | |
996 | do_show_log_standalone(log); | |
997 | } else { | |
998 | do_show_log_cluster(log); | |
999 | } | |
1000 | ovsdb_log_close(log); | |
1001 | } | |
1002 | ||
1003 | struct server { | |
6bf2e3f6 | 1004 | struct ovsdb_log *log; |
1b1d2e6d BP |
1005 | const char *filename; |
1006 | const char *nickname; | |
1007 | ||
1008 | struct raft_header header; | |
1009 | ||
1010 | struct raft_record *records; | |
1011 | size_t n_records; | |
1012 | ||
1013 | struct raft_entry *snap; | |
1014 | struct raft_entry *entries; | |
1015 | uint64_t log_start, log_end; | |
1016 | }; | |
1017 | ||
1018 | struct leader { | |
1019 | /* In struct cluster's 'leaders', indexed by 'term'. */ | |
1020 | struct hmap_node hmap_node; | |
1021 | ||
1022 | /* This structure indicates that in 'term', 'server' reported that 'leader' | |
1023 | * was elected leader. When 'log_end' is nonzero, it additionally | |
1024 | * indicates 'leader''s log_end at the time it was elected. */ | |
1025 | uint64_t term; | |
1026 | struct server *server; | |
1027 | struct uuid leader; | |
1028 | uint64_t log_end; | |
1029 | }; | |
1030 | ||
1031 | struct commit { | |
1032 | /* In struct cluster's 'commits', indexed by 'term'. */ | |
1033 | struct hmap_node hmap_node; | |
1034 | ||
1035 | /* This structure indicates that in 'term', 'server' reported the commit | |
1036 | * index as 'index'. */ | |
1037 | uint64_t term; | |
1038 | struct server *server; | |
1039 | uint64_t index; /* Commit index. */ | |
1040 | }; | |
1041 | ||
1042 | struct cluster { | |
1043 | struct server *servers; | |
1044 | size_t n_servers; | |
1045 | ||
1046 | struct hmap leaders; /* Contains 'struct leader's. */ | |
1047 | ||
1048 | struct hmap commits; /* Contains 'struct commit's. */ | |
1049 | }; | |
1050 | ||
1051 | static const char * | |
1052 | get_server_name(const struct cluster *c, const struct uuid *sid, | |
1053 | char buf[SID_LEN + 1], size_t bufsize) | |
1054 | { | |
1055 | for (size_t i = 0; i < c->n_servers; i++) { | |
6233b87d | 1056 | struct server *s = &c->servers[i]; |
1b1d2e6d BP |
1057 | if (uuid_equals(&s->header.sid, sid)) { |
1058 | return s->filename; | |
1059 | } | |
1060 | } | |
1061 | ||
1062 | snprintf(buf, bufsize, SID_FMT, SID_ARGS(sid)); | |
1063 | return buf; | |
1064 | } | |
1065 | ||
1066 | static struct leader * | |
1067 | find_leader(struct cluster *c, uint64_t term) | |
1068 | { | |
1069 | struct leader *leader; | |
1070 | HMAP_FOR_EACH_WITH_HASH (leader, hmap_node, hash_uint64(term), | |
1071 | &c->leaders) { | |
1072 | if (term == leader->term) { | |
1073 | return leader; | |
1074 | } | |
1075 | } | |
1076 | return NULL; | |
1077 | } | |
1078 | ||
1079 | /* Records that 'server' reported that 'leader' was elected leader in 'term'. | |
1080 | * | |
1081 | * Checks the Election Safety Property: at most one leader may be elected in a | |
1082 | * single term (see Figure 3.2). */ | |
1083 | static void | |
1084 | record_leader(struct cluster *c, uint64_t term, struct server *server, | |
1085 | const struct uuid *leader) | |
1086 | { | |
1087 | bool server_is_leader = uuid_equals(&server->header.sid, leader); | |
1088 | struct leader *p = find_leader(c, term); | |
1089 | if (p) { | |
1090 | if (!uuid_equals(&p->leader, leader)) { | |
1091 | char buf1[SID_LEN + 1]; | |
1092 | char buf2[SID_LEN + 1]; | |
1093 | ovs_fatal(0, "term %"PRIu64" has two different leaders: " | |
1094 | "%s says that the leader is %s and " | |
1095 | "%s says that the leader is %s", | |
1096 | term, | |
1097 | p->server->filename, | |
1098 | get_server_name(c, &p->leader, buf1, sizeof buf1), | |
1099 | server->filename, | |
1100 | get_server_name(c, leader, buf2, sizeof buf2)); | |
1101 | } | |
1102 | if (server_is_leader && server->log_end > p->log_end) { | |
1103 | p->log_end = server->log_end; | |
1104 | } | |
1105 | } else { | |
1106 | p = xmalloc(sizeof *p); | |
1107 | hmap_insert(&c->leaders, &p->hmap_node, hash_uint64(term)); | |
1108 | p->term = term; | |
1109 | p->server = server; | |
1110 | p->leader = *leader; | |
1111 | if (server_is_leader) { | |
1112 | p->log_end = server->log_end; | |
1113 | } else { | |
1114 | p->log_end = 0; | |
1115 | } | |
1116 | } | |
1117 | } | |
1118 | ||
1119 | static struct commit * | |
1120 | find_commit(struct cluster *c, uint64_t term) | |
1121 | { | |
1122 | struct commit *commit; | |
1123 | HMAP_FOR_EACH_WITH_HASH (commit, hmap_node, hash_uint64(term), | |
1124 | &c->commits) { | |
1125 | if (term == commit->term) { | |
1126 | return commit; | |
1127 | } | |
1128 | } | |
1129 | return NULL; | |
1130 | } | |
1131 | ||
1132 | static void | |
1133 | record_commit(struct cluster *c, uint64_t term, struct server *server, | |
1134 | uint64_t commit_index) | |
1135 | { | |
1136 | struct commit *commit = find_commit(c, term); | |
1137 | if (commit) { | |
1138 | if (commit_index > commit->index) { | |
1139 | commit->server = server; | |
1140 | commit->index = commit_index; | |
1141 | } | |
1142 | } else { | |
1143 | commit = xmalloc(sizeof *commit); | |
1144 | hmap_insert(&c->commits, &commit->hmap_node, hash_uint64(term)); | |
1145 | commit->term = term; | |
1146 | commit->server = server; | |
1147 | commit->index = commit_index; | |
1148 | } | |
1149 | } | |
1150 | ||
1151 | static void | |
1152 | do_check_cluster(struct ovs_cmdl_context *ctx) | |
1153 | { | |
1154 | struct cluster c = { | |
1155 | .servers = xzalloc((ctx->argc - 1) * sizeof *c.servers), | |
1156 | .n_servers = 0, | |
1157 | .leaders = HMAP_INITIALIZER(&c.leaders), | |
1158 | .commits = HMAP_INITIALIZER(&c.commits), | |
1159 | }; | |
1160 | ||
1161 | uint64_t min_term = UINT64_MAX; | |
1162 | uint64_t max_term = 0; | |
1163 | ||
1164 | for (int i = 1; i < ctx->argc; i++) { | |
1165 | struct server *s = &c.servers[c.n_servers]; | |
1166 | s->filename = ctx->argv[i]; | |
1167 | ||
1b1d2e6d | 1168 | check_ovsdb_error(ovsdb_log_open(s->filename, RAFT_MAGIC, |
6bf2e3f6 | 1169 | OVSDB_LOG_READ_ONLY, -1, &s->log)); |
1b1d2e6d BP |
1170 | |
1171 | struct json *json; | |
6bf2e3f6 | 1172 | check_ovsdb_error(ovsdb_log_read(s->log, &json)); |
1b1d2e6d BP |
1173 | check_ovsdb_error(raft_header_from_json(&s->header, json)); |
1174 | json_destroy(json); | |
1175 | ||
1176 | if (s->header.joining) { | |
1177 | printf("%s has not joined the cluster, omitting\n", s->filename); | |
6bf2e3f6 | 1178 | ovsdb_log_close(s->log); |
1b1d2e6d BP |
1179 | continue; |
1180 | } | |
7073a83f BP |
1181 | for (size_t j = 0; j < c.n_servers; j++) { |
1182 | if (uuid_equals(&s->header.sid, &c.servers[j].header.sid)) { | |
1183 | ovs_fatal(0, "Duplicate server ID "SID_FMT" in %s and %s.", | |
1184 | SID_ARGS(&s->header.sid), | |
1185 | s->filename, c.servers[j].filename); | |
1186 | } | |
1187 | } | |
1b1d2e6d BP |
1188 | if (c.n_servers > 0) { |
1189 | struct server *s0 = &c.servers[0]; | |
1190 | if (!uuid_equals(&s0->header.cid, &s->header.cid)) { | |
1191 | ovs_fatal(0, "%s has cluster ID "CID_FMT" but %s " | |
1192 | "has cluster ID "CID_FMT, | |
1193 | s0->filename, CID_ARGS(&s0->header.cid), | |
1194 | s->filename, CID_ARGS(&s->header.cid)); | |
1195 | } | |
1196 | if (strcmp(s0->header.name, s->header.name)) { | |
1197 | ovs_fatal(0, "%s is named \"%s\" but %s is named \"%s\"", | |
1198 | s0->filename, s0->header.name, | |
1199 | s->filename, s->header.name); | |
1200 | } | |
1201 | } | |
6bf2e3f6 BP |
1202 | c.n_servers++; |
1203 | } | |
1204 | ||
1205 | for (struct server *s = c.servers; s < &c.servers[c.n_servers]; s++) { | |
1b1d2e6d BP |
1206 | s->snap = &s->header.snap; |
1207 | s->log_start = s->log_end = s->header.snap_index + 1; | |
1208 | ||
1209 | size_t allocated_records = 0; | |
1210 | size_t allocated_entries = 0; | |
1211 | ||
1212 | uint64_t term = 0; /* Current term. */ | |
1213 | struct uuid vote = UUID_ZERO; /* Server 's''s vote in 'term'. */ | |
1214 | struct uuid leader = UUID_ZERO; /* Cluster leader in 'term'. */ | |
1215 | uint64_t leader_rec_idx = 0; /* Index of last "leader" record. */ | |
1216 | ||
1217 | uint64_t commit_index = s->header.snap_index; | |
1218 | ||
1219 | for (unsigned long long int rec_idx = 1; ; rec_idx++) { | |
1220 | if (s->n_records >= allocated_records) { | |
1221 | s->records = x2nrealloc(s->records, &allocated_records, | |
1222 | sizeof *s->records); | |
1223 | } | |
6bf2e3f6 BP |
1224 | |
1225 | struct json *json; | |
1226 | check_ovsdb_error(ovsdb_log_read(s->log, &json)); | |
1b1d2e6d BP |
1227 | if (!json) { |
1228 | break; | |
1229 | } | |
1230 | struct raft_record *r = &s->records[s->n_records++]; | |
1231 | check_ovsdb_error(raft_record_from_json(r, json)); | |
1232 | json_destroy(json); | |
1233 | ||
1234 | if (r->term > term) { | |
1235 | term = r->term; | |
1236 | vote = UUID_ZERO; | |
1237 | leader = UUID_ZERO; | |
1238 | leader_rec_idx = 0; | |
1239 | } | |
1240 | if (term < min_term) { | |
1241 | min_term = term; | |
1242 | } | |
1243 | if (term > max_term) { | |
1244 | max_term = term; | |
1245 | } | |
1246 | ||
1b1d2e6d BP |
1247 | switch (r->type) { |
1248 | case RAFT_REC_ENTRY: | |
1249 | if (r->entry.index < commit_index) { | |
1250 | ovs_fatal(0, "%s: record %llu attempts to truncate log " | |
1251 | "from %"PRIu64" to %"PRIu64" entries, but " | |
1252 | "commit index is already %"PRIu64, | |
1253 | s->filename, rec_idx, | |
1254 | s->log_end, r->entry.index, | |
1255 | commit_index); | |
1256 | } else if (r->entry.index > s->log_end) { | |
1257 | ovs_fatal(0, "%s: record %llu with index %"PRIu64" skips " | |
1258 | "past expected index %"PRIu64, s->filename, | |
1259 | rec_idx, r->entry.index, s->log_end); | |
1260 | } | |
1261 | ||
1262 | if (r->entry.index < s->log_end) { | |
1263 | bool is_leader = uuid_equals(&s->header.sid, &leader); | |
1264 | if (is_leader) { | |
1265 | /* Leader Append-Only property (see Figure 3.2). */ | |
1266 | ovs_fatal(0, "%s: record %llu truncates log from " | |
1267 | "%"PRIu64" to %"PRIu64" entries while " | |
1268 | "server is leader", s->filename, rec_idx, | |
1269 | s->log_end, r->entry.index); | |
1270 | } else { | |
1271 | /* This can happen, but it is unusual. */ | |
1272 | printf("%s: record %llu truncates log from %"PRIu64 | |
1273 | " to %"PRIu64" entries\n", s->filename, rec_idx, | |
1274 | s->log_end, r->entry.index); | |
1275 | } | |
1276 | s->log_end = r->entry.index; | |
1277 | } | |
1278 | ||
1279 | uint64_t prev_term = (s->log_end > s->log_start | |
1280 | ? s->entries[s->log_end | |
1281 | - s->log_start - 1].term | |
1282 | : s->snap->term); | |
1283 | if (r->term < prev_term) { | |
1284 | ovs_fatal(0, "%s: record %llu with index %"PRIu64" term " | |
1285 | "%"PRIu64" precedes previous entry's term " | |
1286 | "%"PRIu64, s->filename, rec_idx, | |
1287 | r->entry.index, r->term, prev_term); | |
1288 | } | |
1289 | ||
1290 | uint64_t log_idx = s->log_end++ - s->log_start; | |
1291 | if (log_idx >= allocated_entries) { | |
1292 | s->entries = x2nrealloc(s->entries, &allocated_entries, | |
1293 | sizeof *s->entries); | |
1294 | } | |
1295 | struct raft_entry *e = &s->entries[log_idx]; | |
1296 | e->term = r->term; | |
1297 | e->data = r->entry.data; | |
1298 | e->eid = r->entry.eid; | |
1299 | e->servers = r->entry.servers; | |
1300 | break; | |
1301 | ||
1302 | case RAFT_REC_TERM: | |
1303 | break; | |
1304 | ||
1305 | case RAFT_REC_VOTE: | |
1306 | if (r->term < term) { | |
1307 | ovs_fatal(0, "%s: record %llu votes for term %"PRIu64" " | |
1308 | "but current term is %"PRIu64, s->filename, | |
1309 | rec_idx, r->term, term); | |
1310 | } else if (!uuid_is_zero(&vote) | |
1311 | && !uuid_equals(&vote, &r->sid)) { | |
1312 | char buf1[SID_LEN + 1]; | |
1313 | char buf2[SID_LEN + 1]; | |
1314 | ovs_fatal(0, "%s: record %llu votes for %s in term " | |
1315 | "%"PRIu64" but a previous record for the " | |
1316 | "same term voted for %s", s->filename, | |
1317 | rec_idx, | |
1318 | get_server_name(&c, &vote, buf1, sizeof buf1), | |
1319 | r->term, | |
1320 | get_server_name(&c, &r->sid, buf2, sizeof buf2)); | |
1321 | } else { | |
1322 | vote = r->sid; | |
1323 | } | |
1324 | break; | |
1325 | ||
1326 | case RAFT_REC_NOTE: | |
1327 | if (!strcmp(r->note, "left")) { | |
1328 | printf("%s: record %llu shows that the server left the " | |
1329 | "cluster\n", s->filename, rec_idx); | |
1330 | } | |
1331 | break; | |
1332 | ||
1333 | case RAFT_REC_COMMIT_INDEX: | |
1334 | if (r->commit_index < commit_index) { | |
1335 | ovs_fatal(0, "%s: record %llu regresses commit index " | |
1336 | "from %"PRIu64 " to %"PRIu64, s->filename, | |
1337 | rec_idx, commit_index, r->commit_index); | |
1338 | } else if (r->commit_index >= s->log_end) { | |
1339 | ovs_fatal(0, "%s: record %llu advances commit index to " | |
1340 | "%"PRIu64 " but last log index is %"PRIu64, | |
1341 | s->filename, rec_idx, r->commit_index, | |
1342 | s->log_end - 1); | |
1343 | } else { | |
1344 | commit_index = r->commit_index; | |
1ff1065c PI |
1345 | } |
1346 | ||
1b1d2e6d BP |
1347 | record_commit(&c, term, s, r->commit_index); |
1348 | break; | |
1349 | ||
1350 | case RAFT_REC_LEADER: | |
1351 | if (!uuid_equals(&r->sid, &leader)) { | |
1352 | if (uuid_is_zero(&leader)) { | |
1353 | leader = r->sid; | |
1354 | leader_rec_idx = rec_idx; | |
1355 | } else { | |
1356 | char buf1[SID_LEN + 1]; | |
1357 | char buf2[SID_LEN + 1]; | |
1358 | ovs_fatal(0, "%s: record %llu reports leader %s " | |
1359 | "for term %"PRIu64" but record %"PRIu64" " | |
1360 | "previously reported the leader as %s " | |
1361 | "in that term", | |
1362 | s->filename, rec_idx, | |
1363 | get_server_name(&c, &r->sid, | |
1364 | buf1, sizeof buf1), | |
1365 | term, leader_rec_idx, | |
1366 | get_server_name(&c, &leader, | |
1367 | buf2, sizeof buf2)); | |
1368 | } | |
1369 | } | |
1370 | record_leader(&c, term, s, &r->sid); | |
1371 | break; | |
722f6301 | 1372 | } |
1b1d2e6d BP |
1373 | } |
1374 | ||
6bf2e3f6 BP |
1375 | ovsdb_log_close(s->log); |
1376 | s->log = NULL; | |
1b1d2e6d | 1377 | } |
722f6301 | 1378 | |
1b1d2e6d BP |
1379 | /* Check the Leader Completeness property from Figure 3.2: If a log entry |
1380 | * is committed in a given term, then that entry will be present in the | |
1381 | * logs of the leaders for all higher-numbered terms. */ | |
1382 | if (min_term == UINT64_MAX || max_term == 0) { | |
1383 | ovs_fatal(0, "all logs are empty"); | |
1384 | } | |
1385 | struct commit *commit = NULL; | |
1386 | for (uint64_t term = min_term; term <= max_term; term++) { | |
1387 | struct leader *leader = find_leader(&c, term); | |
c2d71875 BP |
1388 | if (leader && leader->log_end |
1389 | && commit && commit->index >= leader->log_end) { | |
1b1d2e6d BP |
1390 | ovs_fatal(0, "leader %s for term %"PRIu64" has log entries only " |
1391 | "up to index %"PRIu64", but index %"PRIu64" was " | |
1392 | "committed in a previous term (e.g. by %s)", | |
1393 | leader->server->filename, term, leader->log_end - 1, | |
1394 | commit->index, commit->server->filename); | |
1395 | } | |
1396 | ||
1397 | struct commit *next = find_commit(&c, term); | |
1398 | if (next && (!commit || next->index > commit->index)) { | |
1399 | commit = next; | |
1400 | } | |
1401 | } | |
1402 | ||
1403 | /* Section 3.5: Check the Log Matching Property in Figure 3.2: | |
1404 | * | |
1405 | * - If two entries in different logs have the same index and term, then | |
1406 | * they store the same command. | |
1407 | * | |
1408 | * - If two entries in different logs have the same index and term, then | |
1409 | * the logs are identical in all preceding entries. | |
1410 | */ | |
1411 | for (size_t i = 0; i < c.n_servers; i++) { | |
1412 | for (size_t j = 0; j < c.n_servers; j++) { | |
1413 | struct server *a = &c.servers[i]; | |
1414 | struct server *b = &c.servers[j]; | |
1415 | ||
1416 | if (a == b) { | |
1417 | continue; | |
722f6301 | 1418 | } |
c6782bb0 | 1419 | |
1b1d2e6d BP |
1420 | bool must_equal = false; |
1421 | for (uint64_t idx = MIN(a->log_end, b->log_end) - 1; | |
1422 | idx >= MAX(a->log_start, b->log_start); | |
1423 | idx--) { | |
1424 | const struct raft_entry *ae = &a->entries[idx - a->log_start]; | |
1425 | const struct raft_entry *be = &b->entries[idx - b->log_start]; | |
1426 | if (ae->term == be->term) { | |
1427 | must_equal = true; | |
1428 | } | |
1429 | if (!must_equal || raft_entry_equals(ae, be)) { | |
1430 | continue; | |
1431 | } | |
1432 | char *as = json_to_string(raft_entry_to_json(ae), JSSF_SORT); | |
1433 | char *bs = json_to_string(raft_entry_to_json(be), JSSF_SORT); | |
1434 | ovs_fatal(0, "log entries with index %"PRIu64" differ:\n" | |
1435 | "%s has %s\n" | |
1436 | "%s has %s", | |
1437 | idx, a->filename, as, b->filename, bs); | |
c6782bb0 | 1438 | } |
722f6301 | 1439 | } |
722f6301 | 1440 | } |
c6782bb0 | 1441 | |
1b1d2e6d BP |
1442 | /* Clean up. */ |
1443 | ||
1444 | for (size_t i = 0; i < c.n_servers; i++) { | |
1445 | struct server *s = &c.servers[i]; | |
1446 | ||
1447 | raft_header_uninit(&s->header); | |
1448 | for (size_t j = 0; j < s->n_records; j++) { | |
1449 | struct raft_record *r = &s->records[j]; | |
1450 | ||
1451 | raft_record_uninit(r); | |
1452 | } | |
1453 | free(s->records); | |
1454 | free(s->entries); | |
1455 | } | |
1456 | free(c.servers); | |
1457 | ||
1458 | struct commit *next_commit; | |
1459 | HMAP_FOR_EACH_SAFE (commit, next_commit, hmap_node, &c.commits) { | |
1460 | hmap_remove(&c.commits, &commit->hmap_node); | |
1461 | free(commit); | |
1462 | } | |
1463 | hmap_destroy(&c.commits); | |
1464 | ||
1465 | struct leader *leader, *next_leader; | |
1466 | HMAP_FOR_EACH_SAFE (leader, next_leader, hmap_node, &c.leaders) { | |
1467 | hmap_remove(&c.leaders, &leader->hmap_node); | |
1468 | free(leader); | |
1469 | } | |
1470 | hmap_destroy(&c.leaders); | |
722f6301 BP |
1471 | } |
1472 | ||
1b1d2e6d BP |
1473 | static struct ovsdb_version |
1474 | parse_version(const char *s) | |
1475 | { | |
1476 | struct ovsdb_version version; | |
1477 | if (!ovsdb_parse_version(s, &version)) { | |
1478 | ovs_fatal(0, "%s: not an OVSDB version number in format x.y.z", s); | |
1479 | } | |
1480 | return version; | |
1481 | } | |
1482 | ||
1483 | static void | |
1484 | do_compare_versions(struct ovs_cmdl_context *ctx) | |
1485 | { | |
1486 | struct ovsdb_version a = parse_version(ctx->argv[1]); | |
1487 | struct ovsdb_version b = parse_version(ctx->argv[3]); | |
1488 | int cmp = (a.x != b.x ? (a.x > b.x ? 1 : -1) | |
1489 | : a.y != b.y ? (a.y > b.y ? 1 : -1) | |
1490 | : a.z != b.z ? (a.z > b.z ? 1 : -1) | |
1491 | : 0); | |
1492 | ||
1493 | const char *op = ctx->argv[2]; | |
1494 | bool result; | |
1495 | if (!strcmp(op, "<")) { | |
1496 | result = cmp < 0; | |
1497 | } else if (!strcmp(op, "<=")) { | |
1498 | result = cmp <= 0; | |
1499 | } else if (!strcmp(op, "==")) { | |
1500 | result = cmp == 0; | |
1501 | } else if (!strcmp(op, ">=")) { | |
1502 | result = cmp >= 0; | |
1503 | } else if (!strcmp(op, ">")) { | |
1504 | result = cmp > 0; | |
1505 | } else if (!strcmp(op, "!=")) { | |
1506 | result = cmp != 0; | |
1507 | } else { | |
1508 | ovs_fatal(0, "%s: not a relational operator", op); | |
1509 | } | |
1510 | ||
1511 | exit(result ? 0 : 2); | |
1512 | } | |
1513 | ||
1514 | ||
f85f8ebb | 1515 | static void |
1636c761 | 1516 | do_help(struct ovs_cmdl_context *ctx OVS_UNUSED) |
f85f8ebb BP |
1517 | { |
1518 | usage(); | |
1519 | } | |
1520 | ||
451de37e | 1521 | static void |
1636c761 | 1522 | do_list_commands(struct ovs_cmdl_context *ctx OVS_UNUSED) |
451de37e | 1523 | { |
5f383751 | 1524 | ovs_cmdl_print_commands(get_all_commands()); |
451de37e AW |
1525 | } |
1526 | ||
5f383751 | 1527 | static const struct ovs_cmdl_command all_commands[] = { |
1f4a7252 | 1528 | { "create", "[db [schema]]", 0, 2, do_create, OVS_RW }, |
1b1d2e6d BP |
1529 | { "create-cluster", "db contents local", 3, 3, do_create_cluster, OVS_RW }, |
1530 | { "join-cluster", "db name local remote...", 4, INT_MAX, do_join_cluster, | |
1531 | OVS_RW }, | |
1f4a7252 RM |
1532 | { "compact", "[db [dst]]", 0, 2, do_compact, OVS_RW }, |
1533 | { "convert", "[db [schema [dst]]]", 0, 3, do_convert, OVS_RW }, | |
1534 | { "needs-conversion", NULL, 0, 2, do_needs_conversion, OVS_RO }, | |
439902cb | 1535 | { "db-name", "[db]", 0, 1, do_db_name, OVS_RO }, |
1f4a7252 RM |
1536 | { "db-version", "[db]", 0, 1, do_db_version, OVS_RO }, |
1537 | { "db-cksum", "[db]", 0, 1, do_db_cksum, OVS_RO }, | |
1b1d2e6d BP |
1538 | { "db-cid", "db", 1, 1, do_db_cid, OVS_RO }, |
1539 | { "db-sid", "db", 1, 1, do_db_sid, OVS_RO }, | |
1540 | { "db-local-address", "db", 1, 1, do_db_local_address, OVS_RO }, | |
1541 | { "db-is-clustered", "db", 1, 1, do_db_is_clustered, OVS_RO }, | |
1542 | { "db-is-standalone", "db", 1, 1, do_db_is_standalone, OVS_RO }, | |
439902cb | 1543 | { "schema-name", "[schema]", 0, 1, do_schema_name, OVS_RO }, |
1f4a7252 RM |
1544 | { "schema-version", "[schema]", 0, 1, do_schema_version, OVS_RO }, |
1545 | { "schema-cksum", "[schema]", 0, 1, do_schema_cksum, OVS_RO }, | |
1546 | { "query", "[db] trns", 1, 2, do_query, OVS_RO }, | |
1547 | { "transact", "[db] trns", 1, 2, do_transact, OVS_RO }, | |
1548 | { "show-log", "[db]", 0, 1, do_show_log, OVS_RO }, | |
1b1d2e6d BP |
1549 | { "check-cluster", "db...", 1, INT_MAX, do_check_cluster, OVS_RO }, |
1550 | { "compare-versions", "a op b", 3, 3, do_compare_versions, OVS_RO }, | |
1f4a7252 RM |
1551 | { "help", NULL, 0, INT_MAX, do_help, OVS_RO }, |
1552 | { "list-commands", NULL, 0, INT_MAX, do_list_commands, OVS_RO }, | |
1553 | { NULL, NULL, 0, 0, NULL, OVS_RO }, | |
f85f8ebb | 1554 | }; |
3815d6c2 | 1555 | |
5f383751 | 1556 | static const struct ovs_cmdl_command *get_all_commands(void) |
3815d6c2 LS |
1557 | { |
1558 | return all_commands; | |
1559 | } |