main.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. #include <smartbotic/database/client.hpp>
  2. #include <nlohmann/json.hpp>
  3. #include "storage/subdb_placement.hpp"
  4. #include <openssl/bio.h>
  5. #include <openssl/bn.h>
  6. #include <openssl/err.h>
  7. #include <openssl/evp.h>
  8. #include <openssl/pem.h>
  9. #include <openssl/rand.h>
  10. #include <openssl/x509.h>
  11. #include <openssl/x509v3.h>
  12. #include <arpa/inet.h>
  13. #include <fcntl.h>
  14. #include <sys/random.h>
  15. #include <sys/stat.h>
  16. #include <unistd.h>
  17. #include <cerrno>
  18. #include <cstdio>
  19. #include <cstring>
  20. #include <fstream>
  21. #include <iostream>
  22. #include <sstream>
  23. #include <string>
  24. #include <vector>
  25. using json = nlohmann::json;
  26. namespace {
  27. struct Args {
  28. std::string address = "localhost:9004";
  29. std::string command;
  30. std::vector<std::string> params;
  31. };
  32. // ANSI colors
  33. constexpr auto C_RESET = "\033[0m";
  34. constexpr auto C_BOLD = "\033[1m";
  35. constexpr auto C_DIM = "\033[2m";
  36. constexpr auto C_CYAN = "\033[36m";
  37. constexpr auto C_GREEN = "\033[32m";
  38. constexpr auto C_RED = "\033[31m";
  39. constexpr auto C_YELLOW = "\033[33m";
  40. void printJson(const json& j) {
  41. std::cout << j.dump(2) << "\n";
  42. }
  43. void printError(const std::string& msg) {
  44. std::cerr << C_RED << "error: " << C_RESET << msg << "\n";
  45. }
  46. void printUsage() {
  47. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET << " — admin tool for smartbotic-database\n\n"
  48. << C_BOLD << "Usage:" << C_RESET << "\n"
  49. << " smartbotic-db-cli [--address HOST:PORT] <command> [args...]\n"
  50. << " smartbotic-db-cli [--address HOST:PORT] " << C_DIM << "# interactive mode" << C_RESET << "\n\n"
  51. << C_BOLD << "Commands:" << C_RESET << "\n"
  52. << " " << C_CYAN << "collections" << C_RESET << " List all collections\n"
  53. << " " << C_CYAN << "info" << C_RESET << " <collection> Collection info\n"
  54. << " " << C_CYAN << "find" << C_RESET << " <collection> List documents\n"
  55. << " " << C_CYAN << "get" << C_RESET << " <collection> <id> Get a document\n"
  56. << " " << C_CYAN << "upsert" << C_RESET << " <collection> <id> '<json>' Insert or update\n"
  57. << " " << C_CYAN << "remove" << C_RESET << " <collection> <id> Delete a document\n"
  58. << " " << C_CYAN << "count" << C_RESET << " <collection> Count documents\n"
  59. << " " << C_CYAN << "lock" << C_RESET << " Lock database (read-only)\n"
  60. << " " << C_CYAN << "unlock" << C_RESET << " Unlock database (accept writes)\n"
  61. << " " << C_CYAN << "status" << C_RESET << " Show read-only + recovery status\n"
  62. << " " << C_CYAN << "help" << C_RESET << " Show this help\n\n"
  63. << C_BOLD << "Offline helpers" << C_RESET << " " << C_DIM << "(no server connection required)" << C_RESET << ":\n"
  64. << " " << C_CYAN << "generate-auth-key" << C_RESET << " Emit a base64 32-byte API key\n"
  65. << " " << C_CYAN << "generate-tls-cert" << C_RESET << " --bind <addr> Generate a self-signed TLS cert + key\n"
  66. << " " << C_DIM << "[--out-cert PATH] [--out-key PATH] [--days N]" << C_RESET << "\n"
  67. << " " << C_CYAN << "verify-subdbs" << C_RESET << " --env <path> Report documents filed under the wrong collection\n"
  68. << " " << C_DIM << "[--project NAME] read-only; safe against a running server" << C_RESET << "\n"
  69. << " " << C_CYAN << "reconcile-subdbs" << C_RESET << " --env <path> Repair misfiled documents (dry run by default)\n"
  70. << " " << C_DIM << "[--project NAME] [--apply] STOP THE SERVICE and back up before --apply" << C_RESET << "\n\n"
  71. << C_BOLD << "Options:" << C_RESET << "\n"
  72. << " --address HOST:PORT Database address (default: localhost:9004)\n";
  73. }
  74. // ---------------------------------------------------------------------------
  75. // v2.4 Stage F: offline helpers (no server connection required)
  76. // ---------------------------------------------------------------------------
  77. // Base64-encode raw bytes. Uses OpenSSL's EVP_EncodeBlock which emits the
  78. // standard base64 alphabet (no newlines) and pads with '='.
  79. std::string base64Encode(const unsigned char* data, size_t len) {
  80. if (len == 0) return {};
  81. // EVP_EncodeBlock writes ((len + 2) / 3) * 4 bytes + a NUL terminator.
  82. const size_t out_len = 4 * ((len + 2) / 3);
  83. std::string out(out_len, '\0');
  84. int written = EVP_EncodeBlock(
  85. reinterpret_cast<unsigned char*>(out.data()),
  86. data, static_cast<int>(len));
  87. if (written < 0) return {};
  88. out.resize(static_cast<size_t>(written));
  89. return out;
  90. }
  91. // Pull 32 cryptographically random bytes from getentropy() (preferred) or
  92. // /dev/urandom (fallback). Returns true on success.
  93. bool fillRandomBytes(unsigned char* buf, size_t len) {
  94. // getentropy() is limited to 256 bytes per call; our use-case is 32.
  95. if (len <= 256) {
  96. if (getentropy(buf, len) == 0) return true;
  97. }
  98. // Fallback: /dev/urandom.
  99. int fd = ::open("/dev/urandom", O_RDONLY | O_CLOEXEC);
  100. if (fd < 0) return false;
  101. size_t got = 0;
  102. while (got < len) {
  103. ssize_t n = ::read(fd, buf + got, len - got);
  104. if (n <= 0) {
  105. if (errno == EINTR) continue;
  106. ::close(fd);
  107. return false;
  108. }
  109. got += static_cast<size_t>(n);
  110. }
  111. ::close(fd);
  112. return true;
  113. }
  114. int cmdGenerateAuthKey() {
  115. unsigned char key[32];
  116. if (!fillRandomBytes(key, sizeof(key))) {
  117. std::fprintf(stderr, "error: failed to obtain entropy for key\n");
  118. return 1;
  119. }
  120. auto encoded = base64Encode(key, sizeof(key));
  121. if (encoded.empty()) {
  122. std::fprintf(stderr, "error: base64 encoding failed\n");
  123. return 1;
  124. }
  125. std::cout << encoded << "\n";
  126. return 0;
  127. }
  128. namespace {
  129. // Print the topmost OpenSSL error to stderr with a prefix.
  130. void printOpenSslError(const char* prefix) {
  131. unsigned long e = ERR_get_error();
  132. char buf[256] = {0};
  133. if (e != 0) {
  134. ERR_error_string_n(e, buf, sizeof(buf));
  135. std::fprintf(stderr, "error: %s: %s\n", prefix, buf);
  136. } else {
  137. std::fprintf(stderr, "error: %s\n", prefix);
  138. }
  139. }
  140. // Returns true if `s` parses as an IPv4 or IPv6 literal.
  141. bool looksLikeIp(const std::string& s) {
  142. unsigned char buf[16];
  143. if (inet_pton(AF_INET, s.c_str(), buf) == 1) return true;
  144. if (inet_pton(AF_INET6, s.c_str(), buf) == 1) return true;
  145. return false;
  146. }
  147. // Generate an RSA private key as an EVP_PKEY using the modern (3.x) API.
  148. EVP_PKEY* generateRsaKey(int bits) {
  149. EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
  150. if (!ctx) return nullptr;
  151. EVP_PKEY* pkey = nullptr;
  152. if (EVP_PKEY_keygen_init(ctx) <= 0) {
  153. EVP_PKEY_CTX_free(ctx);
  154. return nullptr;
  155. }
  156. if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0) {
  157. EVP_PKEY_CTX_free(ctx);
  158. return nullptr;
  159. }
  160. if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
  161. EVP_PKEY_CTX_free(ctx);
  162. return nullptr;
  163. }
  164. EVP_PKEY_CTX_free(ctx);
  165. return pkey;
  166. }
  167. // Write a string to disk with the given file mode. Truncates existing files.
  168. bool writeFileWithMode(const std::string& path, const std::string& contents, mode_t mode) {
  169. int fd = ::open(path.c_str(),
  170. O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
  171. mode);
  172. if (fd < 0) {
  173. std::fprintf(stderr, "error: open %s: %s\n", path.c_str(), std::strerror(errno));
  174. return false;
  175. }
  176. // Re-assert mode in case the file pre-existed with a wider mode and
  177. // O_CREAT was a no-op (open(2) only applies the mode on create).
  178. if (fchmod(fd, mode) != 0) {
  179. std::fprintf(stderr, "error: fchmod %s: %s\n", path.c_str(), std::strerror(errno));
  180. ::close(fd);
  181. return false;
  182. }
  183. size_t off = 0;
  184. while (off < contents.size()) {
  185. ssize_t n = ::write(fd, contents.data() + off, contents.size() - off);
  186. if (n <= 0) {
  187. if (errno == EINTR) continue;
  188. std::fprintf(stderr, "error: write %s: %s\n", path.c_str(), std::strerror(errno));
  189. ::close(fd);
  190. return false;
  191. }
  192. off += static_cast<size_t>(n);
  193. }
  194. if (::close(fd) != 0) {
  195. std::fprintf(stderr, "error: close %s: %s\n", path.c_str(), std::strerror(errno));
  196. return false;
  197. }
  198. return true;
  199. }
  200. // Serialize an X509* to a PEM string.
  201. std::string pemEncodeCert(X509* cert) {
  202. BIO* bio = BIO_new(BIO_s_mem());
  203. if (!bio) return {};
  204. if (PEM_write_bio_X509(bio, cert) != 1) {
  205. BIO_free(bio);
  206. return {};
  207. }
  208. BUF_MEM* mem = nullptr;
  209. BIO_get_mem_ptr(bio, &mem);
  210. std::string out(mem->data, mem->length);
  211. BIO_free(bio);
  212. return out;
  213. }
  214. // Serialize an EVP_PKEY* to an unencrypted PEM string (PKCS#8 format via
  215. // PEM_write_bio_PrivateKey).
  216. std::string pemEncodeKey(EVP_PKEY* key) {
  217. BIO* bio = BIO_new(BIO_s_mem());
  218. if (!bio) return {};
  219. if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr, nullptr) != 1) {
  220. BIO_free(bio);
  221. return {};
  222. }
  223. BUF_MEM* mem = nullptr;
  224. BIO_get_mem_ptr(bio, &mem);
  225. std::string out(mem->data, mem->length);
  226. BIO_free(bio);
  227. return out;
  228. }
  229. } // anonymous namespace
  230. int cmdGenerateTlsCert(const std::vector<std::string>& params) {
  231. std::string bind;
  232. std::string out_cert = "./server.pem";
  233. std::string out_key = "./server.key";
  234. int days = 3650;
  235. for (size_t i = 0; i < params.size(); ++i) {
  236. const auto& p = params[i];
  237. auto need = [&](const char* flag) -> const std::string* {
  238. if (i + 1 >= params.size()) {
  239. std::fprintf(stderr, "error: %s requires a value\n", flag);
  240. return nullptr;
  241. }
  242. return &params[++i];
  243. };
  244. if (p == "--bind") {
  245. auto* v = need("--bind"); if (!v) return 2; bind = *v;
  246. } else if (p == "--out-cert") {
  247. auto* v = need("--out-cert"); if (!v) return 2; out_cert = *v;
  248. } else if (p == "--out-key") {
  249. auto* v = need("--out-key"); if (!v) return 2; out_key = *v;
  250. } else if (p == "--days") {
  251. auto* v = need("--days"); if (!v) return 2;
  252. try { days = std::stoi(*v); }
  253. catch (...) {
  254. std::fprintf(stderr, "error: --days must be an integer\n");
  255. return 2;
  256. }
  257. if (days <= 0) {
  258. std::fprintf(stderr, "error: --days must be > 0\n");
  259. return 2;
  260. }
  261. } else {
  262. std::fprintf(stderr, "error: unknown argument: %s\n", p.c_str());
  263. return 2;
  264. }
  265. }
  266. if (bind.empty()) {
  267. std::fprintf(stderr,
  268. "usage: generate-tls-cert --bind <addr> "
  269. "[--out-cert PATH] [--out-key PATH] [--days N]\n");
  270. return 2;
  271. }
  272. // 1. RSA 4096-bit key.
  273. EVP_PKEY* pkey = generateRsaKey(4096);
  274. if (!pkey) {
  275. printOpenSslError("RSA key generation failed");
  276. return 1;
  277. }
  278. // 2. X.509 certificate.
  279. X509* cert = X509_new();
  280. if (!cert) {
  281. printOpenSslError("X509_new failed");
  282. EVP_PKEY_free(pkey);
  283. return 1;
  284. }
  285. // Version 3 (the integer field encodes v3 as 2).
  286. if (X509_set_version(cert, 2) != 1) {
  287. printOpenSslError("X509_set_version failed");
  288. X509_free(cert); EVP_PKEY_free(pkey);
  289. return 1;
  290. }
  291. // Random 64-bit serial number.
  292. {
  293. unsigned char serial_bytes[8];
  294. if (RAND_bytes(serial_bytes, sizeof(serial_bytes)) != 1) {
  295. printOpenSslError("RAND_bytes for serial failed");
  296. X509_free(cert); EVP_PKEY_free(pkey);
  297. return 1;
  298. }
  299. // Mask the top bit so the BIGNUM is positive.
  300. serial_bytes[0] &= 0x7F;
  301. BIGNUM* bn = BN_bin2bn(serial_bytes, sizeof(serial_bytes), nullptr);
  302. if (!bn) {
  303. printOpenSslError("BN_bin2bn failed");
  304. X509_free(cert); EVP_PKEY_free(pkey);
  305. return 1;
  306. }
  307. ASN1_INTEGER* ai = BN_to_ASN1_INTEGER(bn, nullptr);
  308. BN_free(bn);
  309. if (!ai) {
  310. printOpenSslError("BN_to_ASN1_INTEGER failed");
  311. X509_free(cert); EVP_PKEY_free(pkey);
  312. return 1;
  313. }
  314. if (X509_set_serialNumber(cert, ai) != 1) {
  315. printOpenSslError("X509_set_serialNumber failed");
  316. ASN1_INTEGER_free(ai);
  317. X509_free(cert); EVP_PKEY_free(pkey);
  318. return 1;
  319. }
  320. ASN1_INTEGER_free(ai);
  321. }
  322. // Validity period.
  323. if (!X509_gmtime_adj(X509_get_notBefore(cert), 0)) {
  324. printOpenSslError("X509_gmtime_adj(notBefore) failed");
  325. X509_free(cert); EVP_PKEY_free(pkey);
  326. return 1;
  327. }
  328. long seconds = static_cast<long>(days) * 24L * 60L * 60L;
  329. if (!X509_gmtime_adj(X509_get_notAfter(cert), seconds)) {
  330. printOpenSslError("X509_gmtime_adj(notAfter) failed");
  331. X509_free(cert); EVP_PKEY_free(pkey);
  332. return 1;
  333. }
  334. // Public key.
  335. if (X509_set_pubkey(cert, pkey) != 1) {
  336. printOpenSslError("X509_set_pubkey failed");
  337. X509_free(cert); EVP_PKEY_free(pkey);
  338. return 1;
  339. }
  340. // Subject + issuer name (self-signed, so identical).
  341. X509_NAME* name = X509_get_subject_name(cert);
  342. if (X509_NAME_add_entry_by_txt(
  343. name, "CN", MBSTRING_UTF8,
  344. reinterpret_cast<const unsigned char*>(bind.c_str()),
  345. -1, -1, 0) != 1) {
  346. printOpenSslError("X509_NAME_add_entry_by_txt(CN) failed");
  347. X509_free(cert); EVP_PKEY_free(pkey);
  348. return 1;
  349. }
  350. if (X509_set_issuer_name(cert, name) != 1) {
  351. printOpenSslError("X509_set_issuer_name failed");
  352. X509_free(cert); EVP_PKEY_free(pkey);
  353. return 1;
  354. }
  355. // SubjectAltName.
  356. {
  357. std::string san = "DNS:localhost,IP:127.0.0.1";
  358. if (bind != "localhost" && bind != "127.0.0.1") {
  359. san += ',';
  360. san += looksLikeIp(bind) ? "IP:" : "DNS:";
  361. san += bind;
  362. }
  363. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  364. nullptr, nullptr, NID_subject_alt_name, san.c_str());
  365. if (!ext) {
  366. printOpenSslError("X509V3_EXT_conf_nid(SAN) failed");
  367. X509_free(cert); EVP_PKEY_free(pkey);
  368. return 1;
  369. }
  370. if (X509_add_ext(cert, ext, -1) != 1) {
  371. printOpenSslError("X509_add_ext(SAN) failed");
  372. X509_EXTENSION_free(ext);
  373. X509_free(cert); EVP_PKEY_free(pkey);
  374. return 1;
  375. }
  376. X509_EXTENSION_free(ext);
  377. }
  378. // basicConstraints CA:FALSE — a server leaf cert, not a CA.
  379. {
  380. X509_EXTENSION* ext = X509V3_EXT_conf_nid(
  381. nullptr, nullptr, NID_basic_constraints, "critical,CA:FALSE");
  382. if (ext) {
  383. X509_add_ext(cert, ext, -1);
  384. X509_EXTENSION_free(ext);
  385. }
  386. }
  387. // Sign with SHA-256.
  388. if (X509_sign(cert, pkey, EVP_sha256()) == 0) {
  389. printOpenSslError("X509_sign failed");
  390. X509_free(cert); EVP_PKEY_free(pkey);
  391. return 1;
  392. }
  393. // Serialize.
  394. std::string cert_pem = pemEncodeCert(cert);
  395. std::string key_pem = pemEncodeKey(pkey);
  396. X509_free(cert);
  397. EVP_PKEY_free(pkey);
  398. if (cert_pem.empty() || key_pem.empty()) {
  399. std::fprintf(stderr, "error: PEM encoding failed\n");
  400. return 1;
  401. }
  402. // Write key first (0600), then cert (0644). If either fails, the other
  403. // may have been written — that's acceptable; the operator will see the
  404. // error and retry.
  405. if (!writeFileWithMode(out_key, key_pem, 0600)) return 1;
  406. if (!writeFileWithMode(out_cert, cert_pem, 0644)) return 1;
  407. std::cout << "Wrote cert: " << out_cert << "\n"
  408. << "Wrote key: " << out_key << "\n";
  409. return 0;
  410. }
  411. bool execCommand(smartbotic::database::Client& client,
  412. const std::string& cmd, const std::vector<std::string>& params) {
  413. try {
  414. if (cmd == "collections") {
  415. auto collections = client.listCollections();
  416. std::cout << C_BOLD << "Collections:" << C_RESET << "\n";
  417. for (const auto& c : collections) {
  418. auto info = client.getCollectionInfo(c);
  419. int64_t count = 0;
  420. if (info) count = info->documentCount;
  421. std::cout << " " << C_CYAN << c << C_RESET
  422. << C_DIM << " (" << count << " docs)" << C_RESET << "\n";
  423. }
  424. return true;
  425. }
  426. if (cmd == "info") {
  427. if (params.empty()) { printError("usage: info <collection>"); return false; }
  428. auto info = client.getCollectionInfo(params[0]);
  429. if (!info) { printError("collection not found: " + params[0]); return false; }
  430. std::cout << C_BOLD << params[0] << C_RESET << ":\n"
  431. << " documents: " << info->documentCount << "\n"
  432. << " size: " << info->sizeBytes << " bytes\n"
  433. << " encrypted: " << (info->encrypted ? "yes" : "no") << "\n"
  434. << " max_versions: " << info->maxVersions << "\n";
  435. if (info->defaultTtlSeconds > 0)
  436. std::cout << " ttl: " << info->defaultTtlSeconds << "s\n";
  437. return true;
  438. }
  439. if (cmd == "find") {
  440. if (params.empty()) { printError("usage: find <collection> [--limit N] [--exists FIELD]"); return false; }
  441. smartbotic::database::Client::QueryOptions opts;
  442. opts.limit = 100;
  443. for (size_t i = 1; i < params.size(); ++i) {
  444. if (params[i] == "--limit" && i + 1 < params.size()) {
  445. opts.limit = static_cast<uint32_t>(std::stoul(params[++i]));
  446. } else if (params[i] == "--exists" && i + 1 < params.size()) {
  447. opts.filters.emplace_back(params[++i], smartbotic::database::Client::FilterOp::EXISTS, true);
  448. }
  449. }
  450. auto docs = client.find(params[0], opts);
  451. std::cout << C_DIM << "(" << docs.size() << " documents)" << C_RESET << "\n";
  452. for (const auto& doc : docs) {
  453. auto id = doc.value("_id", "");
  454. // Print compact summary line
  455. std::cout << C_GREEN << id << C_RESET;
  456. // Show a few key fields
  457. for (const auto& [k, v] : doc.items()) {
  458. if (k == "_id" || k == "_created_at" || k == "_updated_at") continue;
  459. auto val = v.dump();
  460. if (val.size() > 60) val = val.substr(0, 57) + "...";
  461. std::cout << " " << C_DIM << k << "=" << C_RESET << val;
  462. // Limit to 3 fields per line
  463. static int field_count = 0;
  464. if (++field_count >= 3) { field_count = 0; break; }
  465. }
  466. std::cout << "\n";
  467. }
  468. return true;
  469. }
  470. if (cmd == "get") {
  471. if (params.size() < 2) { printError("usage: get <collection> <id>"); return false; }
  472. auto doc = client.get(params[0], params[1]);
  473. if (!doc) { printError("not found: " + params[0] + "/" + params[1]); return false; }
  474. printJson(*doc);
  475. return true;
  476. }
  477. if (cmd == "upsert") {
  478. if (params.size() < 3) { printError("usage: upsert <collection> <id> '<json>'"); return false; }
  479. auto data = json::parse(params[2], nullptr, false);
  480. if (!data.is_object()) { printError("invalid JSON: " + params[2]); return false; }
  481. client.upsert(params[0], data, params[1]);
  482. std::cout << C_GREEN << "ok" << C_RESET << " " << params[0] << "/" << params[1] << "\n";
  483. return true;
  484. }
  485. if (cmd == "remove" || cmd == "delete") {
  486. if (params.size() < 2) { printError("usage: remove <collection> <id>"); return false; }
  487. client.remove(params[0], params[1]);
  488. std::cout << C_GREEN << "ok" << C_RESET << " removed " << params[0] << "/" << params[1] << "\n";
  489. return true;
  490. }
  491. if (cmd == "count") {
  492. if (params.empty()) { printError("usage: count <collection>"); return false; }
  493. auto info = client.getCollectionInfo(params[0]);
  494. if (!info) { printError("collection not found: " + params[0]); return false; }
  495. std::cout << info->documentCount << "\n";
  496. return true;
  497. }
  498. if (cmd == "lock") {
  499. if (client.setReadOnly(true)) {
  500. std::cout << C_GREEN << "ok" << C_RESET << " Database locked (read-only)\n";
  501. return true;
  502. }
  503. printError("failed to lock database");
  504. return false;
  505. }
  506. if (cmd == "unlock") {
  507. if (client.setReadOnly(false)) {
  508. std::cout << C_GREEN << "ok" << C_RESET << " Database unlocked (writes accepted)\n";
  509. return true;
  510. }
  511. printError("failed to unlock database");
  512. return false;
  513. }
  514. if (cmd == "status") {
  515. auto s = client.getReadOnlyStatus();
  516. std::cout << "Read-only: " << (s.readOnly ? (std::string(C_RED) + "YES" + C_RESET) : "no") << "\n";
  517. if (s.readOnly) {
  518. std::cout << "Reason: " << s.reason << "\n";
  519. }
  520. std::cout << "Recovery outcome: " << s.recoveryOutcome << "\n";
  521. if (!s.expectedSnapshot.empty()) {
  522. std::cout << "Expected snapshot: " << s.expectedSnapshot << "\n";
  523. }
  524. if (!s.snapshotUsed.empty()) {
  525. std::cout << "Snapshot used: " << s.snapshotUsed << "\n";
  526. }
  527. if (!s.failureReason.empty()) {
  528. std::cout << "Failure reason: " << s.failureReason << "\n";
  529. }
  530. std::cout << "WAL replayed: " << s.walEntriesReplayed << " entries\n";
  531. std::cout << "Snapshots tried: " << s.snapshotsAttempted << "\n";
  532. return true;
  533. }
  534. if (cmd == "help" || cmd == "?") {
  535. printUsage();
  536. return true;
  537. }
  538. printError("unknown command: " + cmd + " (try 'help')");
  539. return false;
  540. } catch (const std::exception& e) {
  541. printError(e.what());
  542. return false;
  543. }
  544. }
  545. // Tokenize a line, respecting single/double quotes and JSON braces
  546. std::vector<std::string> tokenize(const std::string& line) {
  547. std::vector<std::string> tokens;
  548. std::string current;
  549. int brace_depth = 0;
  550. char in_quote = 0;
  551. for (size_t i = 0; i < line.size(); ++i) {
  552. char c = line[i];
  553. if (in_quote) {
  554. current += c;
  555. if (c == in_quote && (i == 0 || line[i-1] != '\\')) {
  556. in_quote = 0;
  557. // Strip surrounding quotes for simple string tokens
  558. if (brace_depth == 0 && current.size() >= 2
  559. && (current.front() == '\'' || current.front() == '"')
  560. && current.front() == current.back()) {
  561. current = current.substr(1, current.size() - 2);
  562. }
  563. }
  564. continue;
  565. }
  566. if (c == '\'' || c == '"') {
  567. in_quote = c;
  568. current += c;
  569. continue;
  570. }
  571. if (c == '{') { brace_depth++; current += c; continue; }
  572. if (c == '}') {
  573. brace_depth--;
  574. current += c;
  575. if (brace_depth <= 0) {
  576. brace_depth = 0;
  577. tokens.push_back(current);
  578. current.clear();
  579. }
  580. continue;
  581. }
  582. if (brace_depth > 0) { current += c; continue; }
  583. if (c == ' ' || c == '\t') {
  584. if (!current.empty()) {
  585. tokens.push_back(current);
  586. current.clear();
  587. }
  588. continue;
  589. }
  590. current += c;
  591. }
  592. if (!current.empty()) tokens.push_back(current);
  593. return tokens;
  594. }
  595. // v2.4.4 — offline sub-db placement audit / repair.
  596. //
  597. // `verify-subdbs` is read-only and safe against a running server.
  598. // `reconcile-subdbs --apply` rewrites document placement and REQUIRES the
  599. // service to be stopped: it holds an LMDB write txn over the whole env.
  600. int cmdSubdbs(const std::string& command, const std::vector<std::string>& params) {
  601. std::string env_path, project;
  602. bool apply = false;
  603. bool stamp = false;
  604. for (size_t i = 0; i < params.size(); ++i) {
  605. const std::string& p = params[i];
  606. if (p == "--env" && i + 1 < params.size()) {
  607. env_path = params[++i];
  608. } else if (p == "--project" && i + 1 < params.size()) {
  609. project = params[++i];
  610. } else if (p == "--apply") {
  611. apply = true;
  612. } else if (p == "--stamp-identity") {
  613. stamp = true;
  614. } else {
  615. printError("unknown argument: " + p);
  616. return 2;
  617. }
  618. }
  619. if (env_path.empty()) {
  620. printError("--env <path> is required (e.g. "
  621. "/var/lib/smartbotic-database/projects/default/env)");
  622. return 2;
  623. }
  624. if (command == "verify-subdbs" && apply) {
  625. printError("--apply is not valid for verify-subdbs; use reconcile-subdbs");
  626. return 2;
  627. }
  628. try {
  629. auto report = smartbotic::db::storage::audit(env_path, project);
  630. smartbotic::db::storage::print_audit(report);
  631. if (command == "verify-subdbs") {
  632. return report.misplaced.empty() ? 0 : 1;
  633. }
  634. if (!apply) {
  635. std::cout << "\n" << C_YELLOW << "Dry run." << C_RESET
  636. << " Re-run with --apply to perform the repair.\n"
  637. << C_DIM
  638. << "Stop the service first, and take a backup: the repair "
  639. "rewrites document placement in place.\n"
  640. << C_RESET;
  641. return report.misplaced.empty() ? 0 : 1;
  642. }
  643. if (stamp) {
  644. std::cout << "\n" << C_YELLOW << "--stamp-identity:" << C_RESET
  645. << " writing sentinels. This REQUIRES the server binary to be"
  646. " v2.4.4 or newer;\n earlier builds do not skip the sentinel"
  647. " and will fail on scan.\n";
  648. }
  649. std::cout << "\nApplying...\n";
  650. auto res = smartbotic::db::storage::repair(env_path, report, stamp);
  651. std::cout << C_GREEN << "done." << C_RESET
  652. << " moved=" << res.moved
  653. << " quarantined=" << res.quarantined
  654. << " stamped=" << res.stamped << "\n";
  655. for (const auto& e : res.errors) {
  656. std::cerr << C_RED << " warn: " << C_RESET << e << "\n";
  657. }
  658. return res.errors.empty() ? 0 : 1;
  659. } catch (const std::exception& e) {
  660. printError(e.what());
  661. return 1;
  662. }
  663. }
  664. } // anonymous namespace
  665. int main(int argc, char* argv[]) {
  666. Args args;
  667. // Parse flags
  668. std::vector<std::string> positional;
  669. for (int i = 1; i < argc; ++i) {
  670. std::string arg = argv[i];
  671. if (arg == "--address" && i + 1 < argc) {
  672. args.address = argv[++i];
  673. } else if (arg == "--help" || arg == "-h") {
  674. printUsage();
  675. return 0;
  676. } else {
  677. positional.push_back(arg);
  678. }
  679. }
  680. if (!positional.empty()) {
  681. args.command = positional[0];
  682. args.params.assign(positional.begin() + 1, positional.end());
  683. }
  684. // Offline helpers — these don't need a running server, so dispatch
  685. // them before opening the gRPC channel.
  686. if (args.command == "generate-auth-key") {
  687. return cmdGenerateAuthKey();
  688. }
  689. if (args.command == "generate-tls-cert") {
  690. return cmdGenerateTlsCert(args.params);
  691. }
  692. if (args.command == "verify-subdbs" || args.command == "reconcile-subdbs") {
  693. return cmdSubdbs(args.command, args.params);
  694. }
  695. // Connect
  696. smartbotic::database::Client client({.address = args.address});
  697. client.connect();
  698. // Scriptable mode: single command
  699. if (!args.command.empty()) {
  700. return execCommand(client, args.command, args.params) ? 0 : 1;
  701. }
  702. // Interactive mode
  703. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET
  704. << " connected to " << C_CYAN << args.address << C_RESET << "\n"
  705. << C_DIM << "Type 'help' for commands, 'exit' to quit." << C_RESET << "\n";
  706. std::string line;
  707. while (true) {
  708. std::cout << C_YELLOW << "> " << C_RESET;
  709. if (!std::getline(std::cin, line)) break;
  710. // Trim
  711. auto start = line.find_first_not_of(" \t");
  712. if (start == std::string::npos) continue;
  713. line = line.substr(start);
  714. if (line == "exit" || line == "quit" || line == "q") break;
  715. if (line.empty()) continue;
  716. auto tokens = tokenize(line);
  717. if (tokens.empty()) continue;
  718. auto cmd = tokens[0];
  719. std::vector<std::string> params(tokens.begin() + 1, tokens.end());
  720. execCommand(client, cmd, params);
  721. }
  722. return 0;
  723. }