1use std::sync::Arc;
25
26use axum::extract::State;
27use axum::http::StatusCode;
28use axum::response::{IntoResponse, Response};
29use axum::Json;
30use mongreldb_cluster::bootstrap::{self, ClusterStatus};
31use mongreldb_cluster::gateway::{self, AdminCommand, JobAction};
32use mongreldb_cluster::node::{ClusterError, NodeIdentity, VersionInfo};
33use mongreldb_types::ids::NodeId;
34use serde::Deserialize;
35use serde_json::{json, Value};
36
37use crate::{request_owner, require_admin, AppState, OptionalPrincipal};
38
39fn component<T: serde::Serialize>(value: &T) -> Value {
42 serde_json::to_value(value).expect("cluster status component serialization")
43}
44
45pub fn cluster_status_json(status: &ClusterStatus) -> Value {
49 let trust = status.trust.as_ref().map(|trust| {
50 json!({
51 "ca_cert_pem": component(&trust.ca_cert_pem),
52 "node_cert_pem": component(&trust.node_cert_pem),
53 "allowed_node_ids": component(&trust.allowed_node_ids),
54 "has_node_key": trust.has_node_key,
55 })
56 });
57 json!({
58 "mode": "cluster",
59 "identity": component(&status.identity),
60 "membership": component(&status.membership),
61 "member_endpoints": component(&status.member_endpoints),
62 "database_group": component(&status.database_group),
63 "trust": trust,
64 "version_info": component(&VersionInfo::current()),
65 })
66}
67
68pub fn standalone_status_json() -> Value {
71 json!({
72 "mode": "standalone",
73 "detail": "no cluster identity in the database directory; \
74 run `mongreldb-server cluster init` or `mongreldb-server cluster join` \
75 to bootstrap one",
76 "version_info": component(&VersionInfo::current()),
77 })
78}
79
80pub fn status_report(node_data: &std::path::Path) -> Result<Value, ClusterError> {
84 match bootstrap::cluster_status(node_data) {
85 Ok(status) => Ok(cluster_status_json(&status)),
86 Err(ClusterError::NotInitialized) => Ok(standalone_status_json()),
87 Err(error) => Err(error),
88 }
89}
90
91pub(crate) fn status_for_cluster_error(error: &ClusterError) -> StatusCode {
96 match error {
97 ClusterError::InvalidInvite(_) | ClusterError::InvalidTrustMaterial(_) => {
98 StatusCode::BAD_REQUEST
99 }
100 ClusterError::InvalidConfirmationToken => StatusCode::FORBIDDEN,
101 ClusterError::NodeNotFound { .. } => StatusCode::NOT_FOUND,
102 ClusterError::NotInitialized
103 | ClusterError::AlreadyBootstrapped { .. }
104 | ClusterError::ClusterIdentityMismatch { .. }
105 | ClusterError::InvalidNodeStateTransition { .. }
106 | ClusterError::BootstrapInProgress(_) => StatusCode::CONFLICT,
107 _ => StatusCode::INTERNAL_SERVER_ERROR,
108 }
109}
110
111fn cluster_error_response(error: &ClusterError) -> Response {
113 (
114 status_for_cluster_error(error),
115 Json(json!({ "error": error.to_string() })),
116 )
117 .into_response()
118}
119
120fn resolve_target_node(state: &AppState, requested: Option<&str>) -> Result<NodeId, Box<Response>> {
125 match requested {
126 Some(text) => text.parse::<NodeId>().map_err(|error| {
127 Box::new(
128 (
129 StatusCode::BAD_REQUEST,
130 Json(json!({ "error": format!("invalid node_id `{text}`: {error}") })),
131 )
132 .into_response(),
133 )
134 }),
135 None => match NodeIdentity::load(state.db.root()) {
136 Ok(Some(identity)) => Ok(identity.node_id),
137 Ok(None) => Err(Box::new(
138 (
139 StatusCode::CONFLICT,
140 Json(json!({
141 "error": "node is standalone; no cluster identity exists \
142 (run `mongreldb-server cluster init` or `cluster join` first)",
143 })),
144 )
145 .into_response(),
146 )),
147 Err(error) => Err(Box::new(cluster_error_response(&error))),
148 },
149 }
150}
151
152pub(crate) async fn status(
158 State(state): State<Arc<AppState>>,
159 OptionalPrincipal(principal): OptionalPrincipal,
160) -> Response {
161 if let Err(response) = require_admin(&state, &principal, "admin.cluster.status") {
162 return *response;
163 }
164 let root = cluster_status_root(&state);
165 match status_report(root) {
166 Ok(mut report) => {
167 if let Some(runtime) = &state.cluster_runtime {
168 match runtime.runtime_status_json().await {
169 Ok(runtime_status) => {
170 if let Some(object) = report.as_object_mut() {
171 object.insert("runtime".into(), runtime_status);
172 }
173 }
174 Err(error) => {
175 if let Some(object) = report.as_object_mut() {
176 object.insert(
177 "runtime".into(),
178 json!({ "live": false, "error": error.to_string() }),
179 );
180 }
181 }
182 }
183 }
184 Json(report).into_response()
185 }
186 Err(error) => {
187 state.audit.record(
188 request_owner(&state, &principal),
189 "admin.cluster.status.fail",
190 error.to_string(),
191 );
192 cluster_error_response(&error)
193 }
194 }
195}
196
197fn cluster_status_root(state: &AppState) -> &std::path::Path {
200 state
201 .cluster_runtime
202 .as_ref()
203 .map(|runtime| runtime.node_data())
204 .unwrap_or_else(|| state.db.root())
205}
206
207fn runtime_not_running_response(command: &str, fields: Value) -> Response {
210 let mut body = fields;
211 if let Some(object) = body.as_object_mut() {
212 object.insert("command".into(), json!(command));
213 object.insert("status".into(), json!("error"));
214 object.insert("error".into(), json!("cluster runtime not running"));
215 object.insert("category".into(), json!("unavailable"));
216 }
217 (StatusCode::SERVICE_UNAVAILABLE, Json(body)).into_response()
218}
219
220fn runtime_op_error_response(command: &str, error: String) -> Response {
222 (
223 StatusCode::CONFLICT,
224 Json(json!({
225 "command": command,
226 "status": "error",
227 "error": error,
228 "category": "failed_precondition",
229 })),
230 )
231 .into_response()
232}
233
234#[derive(Deserialize)]
237pub(crate) struct NodeDrainRequest {
238 #[serde(default)]
239 node_id: Option<String>,
240}
241
242#[derive(Deserialize)]
245pub(crate) struct NodeRemoveRequest {
246 #[serde(default)]
247 node_id: Option<String>,
248 #[serde(default)]
249 confirm_token: Option<String>,
250}
251
252pub(crate) async fn drain(
255 State(state): State<Arc<AppState>>,
256 OptionalPrincipal(principal): OptionalPrincipal,
257 body: Option<Json<NodeDrainRequest>>,
258) -> Response {
259 let owner = match require_admin(&state, &principal, "admin.cluster.drain") {
260 Ok(owner) => owner,
261 Err(response) => return *response,
262 };
263 let requested = body.and_then(|Json(request)| request.node_id);
264 let node_id = match resolve_target_node(&state, requested.as_deref()) {
265 Ok(node_id) => node_id,
266 Err(response) => return *response,
267 };
268 state.audit.record(
269 owner.clone(),
270 "admin.cluster.drain",
271 format!("initiated node_id={node_id}"),
272 );
273 match bootstrap::node_drain(state.db.root(), node_id) {
274 Ok(descriptor) => {
275 state.audit.record(
276 owner,
277 "admin.cluster.drain.ok",
278 format!("node_id={node_id} state={}", descriptor.state),
279 );
280 Json(json!({ "member": component(&descriptor) })).into_response()
281 }
282 Err(error) => {
283 state.audit.record(
284 owner,
285 "admin.cluster.drain.fail",
286 format!("node_id={node_id} {error}"),
287 );
288 cluster_error_response(&error)
289 }
290 }
291}
292
293pub(crate) async fn remove(
298 State(state): State<Arc<AppState>>,
299 OptionalPrincipal(principal): OptionalPrincipal,
300 body: Option<Json<NodeRemoveRequest>>,
301) -> Response {
302 let owner = match require_admin(&state, &principal, "admin.cluster.remove") {
303 Ok(owner) => owner,
304 Err(response) => return *response,
305 };
306 let (requested, confirm_token) = match body {
307 Some(Json(request)) => (request.node_id, request.confirm_token),
308 None => (None, None),
309 };
310 let Some(confirm_token) = confirm_token else {
311 return (
312 StatusCode::BAD_REQUEST,
313 Json(json!({
314 "error": "confirm_token is required; obtain it out of band via \
315 `mongreldb-server node remove --data-dir <dir>`",
316 })),
317 )
318 .into_response();
319 };
320 let node_id = match resolve_target_node(&state, requested.as_deref()) {
321 Ok(node_id) => node_id,
322 Err(response) => return *response,
323 };
324 state.audit.record(
325 owner.clone(),
326 "admin.cluster.remove",
327 format!("initiated node_id={node_id}"),
328 );
329 match bootstrap::node_remove(state.db.root(), node_id, &confirm_token) {
330 Ok(descriptor) => {
331 state.audit.record(
332 owner,
333 "admin.cluster.remove.ok",
334 format!("node_id={node_id} state={}", descriptor.state),
335 );
336 Json(json!({ "member": component(&descriptor) })).into_response()
337 }
338 Err(error) => {
339 state.audit.record(
340 owner,
341 "admin.cluster.remove.fail",
342 format!("node_id={node_id} {error}"),
343 );
344 cluster_error_response(&error)
345 }
346 }
347}
348
349pub(crate) async fn try_admin_sql(
366 state: &AppState,
367 principal: &Option<mongreldb_core::Principal>,
368 sql: &str,
369) -> Option<Response> {
370 let command = match gateway::parse_admin_sql(sql) {
371 Ok(Some(cmd)) => cmd,
372 Ok(None) => return None,
373 Err(error) => {
374 return Some(
375 (
376 StatusCode::BAD_REQUEST,
377 Json(json!({ "error": error.to_string(), "category": "invalid_argument" })),
378 )
379 .into_response(),
380 );
381 }
382 };
383
384 let owner = match require_admin(state, principal, "admin.sql") {
387 Ok(owner) => owner,
388 Err(response) => return Some(*response),
389 };
390 state.audit.record(
391 owner.clone(),
392 "admin.sql",
393 format!("command={}", admin_command_name(&command)),
394 );
395
396 let status_root = cluster_status_root(state);
397 let response = match command {
398 AdminCommand::ShowCluster => match status_report(status_root) {
399 Ok(mut report) => {
400 let multi = state
402 .multi_region
403 .lock()
404 .map(|p| {
405 let placement =
406 mongreldb_cluster::multi_region::placement_from_multi_region(&p);
407 json!({
408 "prefer_home_leader": p.prefer_home_leader,
409 "regional_followers": p.regional_followers,
410 "async_dr_regions": p.async_dr_regions,
411 "tenant_home_region": p.tenant_home_region,
412 "total_voters": p.voters.total_voters(),
413 "placement_replicas": placement.replicas,
414 "multi_leader_default": false,
415 })
416 })
417 .unwrap_or_else(|_| json!({ "error": "multi_region lock poisoned" }));
418 if let Some(runtime) = &state.cluster_runtime {
419 match runtime.runtime_status_json().await {
420 Ok(runtime_status) => {
421 if let Some(object) = report.as_object_mut() {
422 object.insert("runtime".into(), runtime_status);
423 }
424 }
425 Err(error) => {
426 if let Some(object) = report.as_object_mut() {
427 object.insert(
428 "runtime".into(),
429 json!({ "live": false, "error": error.to_string() }),
430 );
431 }
432 }
433 }
434 }
435 Json(json!({
436 "command": "SHOW CLUSTER",
437 "result": report,
438 "multi_region": multi,
439 }))
440 .into_response()
441 }
442 Err(error) => cluster_error_response(&error),
443 },
444 AdminCommand::ShowNodes => match bootstrap::cluster_status(status_root) {
445 Ok(status) => Json(json!({
446 "command": "SHOW NODES",
447 "nodes": component(&status.member_endpoints),
448 "membership": component(&status.membership),
449 }))
450 .into_response(),
451 Err(ClusterError::NotInitialized) => Json(json!({
452 "command": "SHOW NODES",
453 "mode": "standalone",
454 "nodes": [],
455 }))
456 .into_response(),
457 Err(error) => cluster_error_response(&error),
458 },
459 AdminCommand::ShowTablets { table } => {
460 let (tablets, issues) = mongreldb_cluster::tablet::list_tablets_on_disk(status_root)
461 .unwrap_or_else(|e| (Vec::new(), vec![e.to_string()]));
462 let rows: Vec<Value> = tablets
463 .iter()
464 .filter(|t| {
465 table.as_ref().is_none_or(|name| {
467 t.table_id.to_string() == *name || name.eq_ignore_ascii_case("all")
468 })
469 })
470 .map(|t| {
471 json!({
472 "tablet_id": t.tablet_id.to_string(),
473 "table_id": t.table_id.to_string(),
474 "raft_group_id": t.raft_group_id.to_string(),
475 "generation": t.generation,
476 "state": t.state.to_string(),
477 "replicas": t.replicas.len(),
478 "leader_hint": t.leader_hint.map(|n| n.to_string()),
479 })
480 })
481 .collect();
482 Json(json!({
483 "command": "SHOW TABLETS",
484 "table": table,
485 "tablets": rows,
486 "issues": issues,
487 }))
488 .into_response()
489 }
490 AdminCommand::ShowReplicas { tablet_id } => {
491 let (tablets, issues) = mongreldb_cluster::tablet::list_tablets_on_disk(status_root)
492 .unwrap_or_else(|e| (Vec::new(), vec![e.to_string()]));
493 let mut replicas = Vec::new();
494 for t in &tablets {
495 if tablet_id.is_some_and(|id| id != t.tablet_id) {
496 continue;
497 }
498 for r in &t.replicas {
499 replicas.push(json!({
500 "tablet_id": t.tablet_id.to_string(),
501 "node_id": r.node_id.to_string(),
502 "role": r.role.to_string(),
503 "raft_node_id": r.raft_node_id,
504 "counts_toward_quorum": r.role.counts_toward_quorum(),
505 }));
506 }
507 }
508 Json(json!({
509 "command": "SHOW REPLICAS",
510 "tablet_id": tablet_id.map(|id| id.to_string()),
511 "replicas": replicas,
512 "issues": issues,
513 }))
514 .into_response()
515 }
516 AdminCommand::ShowTransactions => {
517 Json(json!({
520 "command": "SHOW TRANSACTIONS",
521 "open_sessions": state.sessions.len(),
522 "transactions": [],
523 "note": "distributed txn status partitions surface when a meta/txn group is hosted; session count is live",
524 }))
525 .into_response()
526 }
527 AdminCommand::ShowQueries => {
528 let queries: Vec<Value> = state
529 .query_registry
530 .active_statuses()
531 .into_iter()
532 .map(|q| {
533 json!({
534 "query_id": format!("{}", q.query_id),
535 "owner": q.owner,
536 "session_id": q.session_id,
537 "phase": format!("{:?}", q.phase),
538 "operation": q.operation,
539 })
540 })
541 .collect();
542 Json(json!({
543 "command": "SHOW QUERIES",
544 "queries": queries,
545 "active_count": state.query_registry.active_count(),
546 }))
547 .into_response()
548 }
549 AdminCommand::ShowJobs => {
550 let engine_jobs: Vec<Value> = state
551 .db
552 .job_registry()
553 .list()
554 .into_iter()
555 .map(|j| {
556 json!({
557 "job_id": j.job_id,
558 "kind": format!("{:?}", j.kind),
559 "state": format!("{:?}", j.state),
560 "source": "engine",
561 })
562 })
563 .collect();
564 let ops: Vec<Value> = state
565 .ops_jobs
566 .lock()
567 .map(|s| {
568 s.list()
569 .into_iter()
570 .map(|j| {
571 json!({
572 "job_id": j.job_id,
573 "kind": j.kind.name(),
574 "state": format!("{:?}", j.state),
575 "phase": j.phase,
576 "progress": j.progress,
577 "source": "ops",
578 })
579 })
580 .collect()
581 })
582 .unwrap_or_default();
583 Json(json!({
584 "command": "SHOW JOBS",
585 "jobs": engine_jobs.into_iter().chain(ops).collect::<Vec<_>>(),
586 }))
587 .into_response()
588 }
589 AdminCommand::ShowResourceGroups => {
590 let stats = Some(state.scheduler.stats());
591 let governor = state.node_governor.lock().ok().map(|mut gov| {
593 let db_gov = state.db.memory_governor();
594 let ai_capacity = std::env::var("MONGRELDB_AI_MAX_CONCURRENT")
595 .ok()
596 .and_then(|v| v.parse().ok())
597 .filter(|v: &usize| *v > 0)
598 .unwrap_or(4);
599 let inputs = crate::admission::build_pressure_inputs(
600 &crate::admission::PressureInputSources {
601 db_reserved_bytes: db_gov.total_used(),
602 db_max_bytes: db_gov.max_bytes(),
603 node_configured_max_bytes: gov.governor.max_bytes(),
604 tablet_reserved_bytes: gov.tablet_reserved_bytes(),
605 ai_capacity,
606 ai_available: state.ai_semaphore.available_permits(),
607 process_rss_bytes: crate::admission::process_rss_bytes(),
608 },
609 );
610 let actions = crate::admission::refresh_pressure(
611 &mut gov,
612 &inputs,
613 &state.scheduler,
614 Some(db_gov),
615 );
616 let pressure = state.scheduler.pressure().snapshot();
617 json!({
618 "tablet_reserved_bytes": gov.tablet_reserved_bytes(),
619 "query_reserved_bytes": inputs.query_reserved_bytes,
620 "configured_max_bytes": inputs.configured_max_bytes,
621 "os_pressure": inputs.os_pressure,
622 "actions": actions.iter().map(|a| format!("{a:?}")).collect::<Vec<_>>(),
623 "applied": {
624 "reject_ai": pressure.reject_ai,
625 "reduced_admission": pressure.reduced_admission,
626 "move_tablet_noops": pressure.move_tablet_noops,
627 "last_evict_bytes": pressure.last_evict_bytes,
628 "evaluate_count": pressure.evaluate_count,
629 },
630 "move_tablet_leaders": "noop_outside_cluster_mode",
632 })
633 });
634 let ai = state.ai_generations.lock().ok().map(|reg| {
636 let local_k = mongreldb_query::adaptive_local_k(10, 2.0, 5);
637 let readiness = mongreldb_core::readiness_action(true, true, false);
638 json!({
639 "adaptive_local_k_example": local_k,
640 "fusion_default": "rrf_k60",
641 "indexes_registered": reg.len(),
642 "readiness_action_example": format!("{readiness:?}"),
643 })
644 });
645 let groups: Vec<Value> = state
646 .resource_groups
647 .names()
648 .into_iter()
649 .filter_map(|name| {
650 state.resource_groups.get(&name).map(|g| {
651 json!({
652 "name": g.name,
653 "max_concurrency": g.max_concurrency,
654 "max_queue": g.max_queue,
655 "memory_bytes": g.memory_bytes,
656 "temporary_disk_bytes": g.temporary_disk_bytes,
657 "work_units": g.work_units,
658 "cpu_weight": g.cpu_weight,
659 "priority": g.priority,
660 "max_result_bytes": g.max_result_bytes,
661 })
662 })
663 })
664 .collect();
665 let embedding_providers = state.embedding_providers.list_ids();
666 Json(json!({
667 "command": "SHOW RESOURCE GROUPS",
668 "resource_groups": groups,
669 "workload_classes": mongreldb_core::WorkloadClass::ALL
670 .iter()
671 .map(|c| c.name())
672 .collect::<Vec<_>>(),
673 "embedding_providers": embedding_providers,
674 "scheduler": stats.map(|s| json!({
675 "tenants": s.tenants,
676 "per_class": s.per_class,
677 })),
678 "node_governor": governor,
679 "ai": ai,
680 }))
681 .into_response()
682 }
683 AdminCommand::ShowBackups => {
684 let backups: Vec<Value> = state
685 .ops_jobs
686 .lock()
687 .map(|s| {
688 s.list()
689 .into_iter()
690 .filter(|j| j.kind == mongreldb_core::OpsJobKind::Backup)
691 .map(|j| {
692 json!({
693 "job_id": j.job_id,
694 "state": format!("{:?}", j.state),
695 "phase": j.phase,
696 "progress": j.progress,
697 "destination": j.params.get("destination"),
698 })
699 })
700 .collect()
701 })
702 .unwrap_or_default();
703 Json(json!({
704 "command": "SHOW BACKUPS",
705 "backups": backups,
706 }))
707 .into_response()
708 }
709 AdminCommand::AlterNodeDrain { node_id } => {
710 state.audit.record(
711 owner.clone(),
712 "admin.sql.drain",
713 format!("node_id={node_id}"),
714 );
715 match bootstrap::node_drain(status_root, node_id) {
716 Ok(descriptor) => {
717 state.audit.record(
718 owner,
719 "admin.sql.drain.ok",
720 format!("node_id={node_id} state={}", descriptor.state),
721 );
722 Json(json!({
723 "command": "ALTER NODE DRAIN",
724 "member": component(&descriptor),
725 }))
726 .into_response()
727 }
728 Err(error) => {
729 state.audit.record(
730 owner,
731 "admin.sql.drain.fail",
732 format!("node_id={node_id} {error}"),
733 );
734 cluster_error_response(&error)
735 }
736 }
737 }
738 AdminCommand::TransferLeader { tablet_id, to } => match &state.cluster_runtime {
739 None => runtime_not_running_response(
740 "TRANSFER LEADER",
741 json!({
742 "tablet_id": tablet_id.to_string(),
743 "to": to.to_string(),
744 }),
745 ),
746 Some(runtime) => match runtime.transfer_leader(tablet_id, to).await {
747 Ok(body) => Json(body).into_response(),
748 Err(error) => runtime_op_error_response("TRANSFER LEADER", error.to_string()),
749 },
750 },
751 AdminCommand::MoveReplica {
752 tablet_id,
753 from,
754 to,
755 } => {
756 let detail = if state.cluster_runtime.is_some() {
760 "move replica is not yet live on the product path"
761 } else {
762 "cluster runtime not running"
763 };
764 (
765 StatusCode::SERVICE_UNAVAILABLE,
766 Json(json!({
767 "command": "MOVE REPLICA",
768 "tablet_id": tablet_id.to_string(),
769 "from": from.to_string(),
770 "to": to.to_string(),
771 "status": "error",
772 "error": detail,
773 "category": "unavailable",
774 })),
775 )
776 .into_response()
777 }
778 AdminCommand::SplitTablet {
779 tablet_id,
780 at_key_hex,
781 } => match &state.cluster_runtime {
782 None => runtime_not_running_response(
783 "SPLIT TABLET",
784 json!({
785 "tablet_id": tablet_id.to_string(),
786 "at_key_hex": at_key_hex,
787 }),
788 ),
789 Some(runtime) => match runtime.split_tablet(tablet_id, at_key_hex).await {
790 Ok(body) => Json(body).into_response(),
791 Err(error) => runtime_op_error_response("SPLIT TABLET", error.to_string()),
792 },
793 },
794 AdminCommand::MergeTablets { left, right } => match &state.cluster_runtime {
795 None => runtime_not_running_response(
796 "MERGE TABLETS",
797 json!({
798 "left": left.to_string(),
799 "right": right.to_string(),
800 }),
801 ),
802 Some(runtime) => match runtime.merge_tablets(left, right).await {
803 Ok(body) => Json(body).into_response(),
804 Err(error) => runtime_op_error_response("MERGE TABLETS", error.to_string()),
805 },
806 },
807 AdminCommand::JobControl { action, job_id } => {
808 let verb = match action {
809 JobAction::Pause => "PAUSE",
810 JobAction::Resume => "RESUME",
811 JobAction::Cancel => "CANCEL",
812 };
813 match action {
816 JobAction::Cancel => {
817 return Some(
818 (
819 StatusCode::SERVICE_UNAVAILABLE,
820 Json(json!({
821 "error": "cancel job is not yet live on the product path",
822 "command": format!("{verb} JOB"),
823 "job_id": job_id,
824 "category": "resource exhausted",
825 "category_code": 18,
826 })),
827 )
828 .into_response(),
829 );
830 }
831 JobAction::Pause | JobAction::Resume => {
832 let result = state.ops_jobs.lock().map(|mut store| match action {
833 JobAction::Pause => store.pause(&job_id).map(|j| j.state),
834 JobAction::Resume => store.start(&job_id).map(|j| j.state),
835 JobAction::Cancel => unreachable!(),
836 });
837 match result {
838 Ok(Ok(job_state)) => Json(json!({
839 "command": format!("{verb} JOB"),
840 "job_id": job_id,
841 "status": "ok",
842 "state": format!("{job_state:?}"),
843 }))
844 .into_response(),
845 Ok(Err(error)) => (
846 StatusCode::BAD_REQUEST,
847 Json(json!({
848 "error": error.to_string(),
849 "command": format!("{verb} JOB"),
850 "job_id": job_id,
851 })),
852 )
853 .into_response(),
854 Err(_) => (
855 StatusCode::INTERNAL_SERVER_ERROR,
856 Json(json!({
857 "error": "ops job store lock poisoned",
858 "command": format!("{verb} JOB"),
859 "job_id": job_id,
860 })),
861 )
862 .into_response(),
863 }
864 }
865 }
866 }
867 AdminCommand::BackupDatabase { destination } => {
868 let mut params = std::collections::BTreeMap::new();
869 if let Some(dest) = &destination {
870 params.insert("destination".into(), dest.clone());
871 }
872 let job = state
873 .ops_jobs
874 .lock()
875 .map(|mut store| store.submit(mongreldb_core::OpsJobKind::Backup, params))
876 .ok();
877 Json(json!({
881 "command": "BACKUP DATABASE",
882 "destination": destination,
883 "status": "accepted",
884 "job": job.map(|j| json!({
885 "job_id": j.job_id,
886 "state": format!("{:?}", j.state),
887 })),
888 "note": "cluster backup protocol is mongreldb_cluster::cluster_backup; job is resumable via ops store",
889 }))
890 .into_response()
891 }
892 AdminCommand::RestoreDatabase {
893 source,
894 disaster_recovery,
895 } => {
896 use mongreldb_cluster::cluster_backup::{
897 load_manifest, plan_restore, RestoreIdentityMode,
898 };
899 use mongreldb_types::ids::{ClusterId, DatabaseId};
900
901 let mut params = std::collections::BTreeMap::new();
902 params.insert("source".into(), source.clone());
903 params.insert("disaster_recovery".into(), disaster_recovery.to_string());
904 let job = state.ops_jobs.lock().ok().map(|mut store| {
905 store.submit(mongreldb_core::OpsJobKind::RestoreVerification, params)
906 });
907
908 let plan_json = match load_manifest(std::path::Path::new(&source)) {
911 Ok(manifest) => {
912 let mode = if disaster_recovery {
913 RestoreIdentityMode::DisasterRecovery
914 } else {
915 RestoreIdentityMode::NewIdentity
916 };
917 let fresh = if disaster_recovery {
918 None
919 } else {
920 Some((ClusterId::new_random(), DatabaseId::new_random()))
921 };
922 match plan_restore(&manifest, mode, fresh) {
923 Ok(plan) => json!({
924 "identity_mode": plan.identity_mode.to_string(),
925 "target_cluster_id": plan.target_cluster_id.to_string(),
926 "target_database_id": plan.target_database_id.to_string(),
927 "source_cluster_id": plan.source_cluster_id.to_string(),
928 "source_database_id": plan.source_database_id.to_string(),
929 "tablet_count": plan.tablets.len(),
930 }),
931 Err(error) => json!({ "error": error.to_string() }),
932 }
933 }
934 Err(error) => json!({
935 "error": error.to_string(),
936 "note": "ops job still accepted; materialize backup path then resume job",
937 }),
938 };
939
940 Json(json!({
942 "command": "RESTORE DATABASE",
943 "source": source,
944 "disaster_recovery": disaster_recovery,
945 "status": "accepted",
946 "job": job.map(|j| json!({
947 "job_id": j.job_id,
948 "kind": j.kind.name(),
949 "state": format!("{:?}", j.state),
950 })),
951 "restore_plan": plan_json,
952 }))
953 .into_response()
954 }
955 };
956 Some(response)
957}
958
959fn admin_command_name(command: &AdminCommand) -> &'static str {
960 match command {
961 AdminCommand::ShowCluster => "SHOW CLUSTER",
962 AdminCommand::ShowNodes => "SHOW NODES",
963 AdminCommand::ShowTablets { .. } => "SHOW TABLETS",
964 AdminCommand::ShowReplicas { .. } => "SHOW REPLICAS",
965 AdminCommand::ShowTransactions => "SHOW TRANSACTIONS",
966 AdminCommand::ShowQueries => "SHOW QUERIES",
967 AdminCommand::ShowJobs => "SHOW JOBS",
968 AdminCommand::ShowResourceGroups => "SHOW RESOURCE GROUPS",
969 AdminCommand::ShowBackups => "SHOW BACKUPS",
970 AdminCommand::AlterNodeDrain { .. } => "ALTER NODE DRAIN",
971 AdminCommand::TransferLeader { .. } => "TRANSFER LEADER",
972 AdminCommand::MoveReplica { .. } => "MOVE REPLICA",
973 AdminCommand::SplitTablet { .. } => "SPLIT TABLET",
974 AdminCommand::MergeTablets { .. } => "MERGE TABLETS",
975 AdminCommand::JobControl { .. } => "JOB CONTROL",
976 AdminCommand::BackupDatabase { .. } => "BACKUP DATABASE",
977 AdminCommand::RestoreDatabase { .. } => "RESTORE DATABASE",
978 }
979}
980
981#[cfg(test)]
982mod tests {
983 use mongreldb_cluster::gateway::parse_admin_sql;
984
985 #[test]
986 fn admin_sql_parser_covers_section_15_surface() {
987 for sql in [
988 "SHOW CLUSTER",
989 "SHOW NODES",
990 "SHOW TABLETS",
991 "SHOW REPLICAS",
992 "SHOW TRANSACTIONS",
993 "SHOW QUERIES",
994 "SHOW JOBS",
995 "SHOW RESOURCE GROUPS",
996 "SHOW BACKUPS",
997 "BACKUP DATABASE",
998 ] {
999 let cmd = parse_admin_sql(sql).unwrap();
1000 assert!(cmd.is_some(), "expected admin command for {sql}");
1001 }
1002 assert!(parse_admin_sql("SELECT 1").unwrap().is_none());
1003 }
1004}