cfg_rollback
Restore a saved OpenSIPS configuration backup to a specified target path, automatically creating a backup of the current config for reversibility, then re-validate with opensips -C to confirm correctness.
Instructions
Restore backup_path to target_path and re-validate.
Before overwriting, the current contents of target_path (if any)
are themselves backed up so this tool is always reversible one more
step. If validate_after=True, the restored config is passed
through opensips -C; a validation failure does NOT undo the
restore (the caller asked for the restore explicitly), but the
result is surfaced so the caller can decide what to do next.
Parameters
backup_path:
Absolute path to the backup file to restore.
target_path:
Absolute path to overwrite. Must live in an allowlisted dir.
validate_after:
Run opensips -C on the restored file. Default True.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| backup_path | Yes | ||
| target_path | Yes | ||
| validate_after | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- MCP tool handler for cfg_rollback. Restores a backup file to target_path with optional validation via opensips -C. Creates a pre-rollback snapshot before overwriting. Decorated with @mcp.tool(), @audited('cfg_rollback'), and @require_permission('config.write').
@mcp.tool() @audited("cfg_rollback") @require_permission("config.write") async def cfg_rollback( ctx: Context, backup_path: str, target_path: str, validate_after: bool = True, ) -> dict[str, Any]: """Restore *backup_path* to *target_path* and re-validate. Before overwriting, the current contents of ``target_path`` (if any) are themselves backed up so this tool is always reversible one more step. If ``validate_after=True``, the restored config is passed through ``opensips -C``; a validation failure does NOT undo the restore (the caller asked for the restore explicitly), but the result is surfaced so the caller can decide what to do next. Parameters ---------- backup_path: Absolute path to the backup file to restore. target_path: Absolute path to overwrite. Must live in an allowlisted dir. validate_after: Run ``opensips -C`` on the restored file. Default ``True``. """ import datetime import shutil if not (os.path.isabs(backup_path) and os.path.isabs(target_path)): return {"error": "backup_path and target_path must both be absolute"} if not os.path.exists(backup_path): return {"error": f"backup_path does not exist: {backup_path}"} if not is_safe_out_dir(os.path.dirname(backup_path)): return {"error": "backup_path is outside the safe dir allowlist"} if not is_safe_out_dir(os.path.dirname(target_path)): return {"error": "target_path is outside the safe dir allowlist"} # Defensive: snapshot the current target so the rollback itself is # reversible. Ignore missing-file — that's fine, nothing to snapshot. pre_rollback_backup: str | None = None if os.path.exists(target_path): ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") pre_rollback_backup = f"{target_path}.pre-rollback-{ts}" try: shutil.copy2(target_path, pre_rollback_backup) except OSError as exc: return {"error": f"pre-rollback snapshot failed: {exc}"} try: shutil.copy2(backup_path, target_path) except OSError as exc: return {"error": f"restore copy failed: {exc}"} out: dict[str, Any] = { "restored_from": backup_path, "restored_to": target_path, "pre_rollback_snapshot": pre_rollback_backup, } if validate_after: try: with open(target_path, encoding="utf-8") as fh: content = fh.read() result = await _validator.validate(content) out["validation"] = { "valid": result.valid, "errors": result.errors, "warnings": result.warnings, } except Exception as exc: out["validation"] = {"error": str(exc)} return out - The tool accepts backup_path (str), target_path (str), and validate_after (bool, default True). Returns a dict with restored_from, restored_to, pre_rollback_snapshot, and optional validation results.
@mcp.tool() @audited("cfg_rollback") @require_permission("config.write") async def cfg_rollback( ctx: Context, backup_path: str, target_path: str, validate_after: bool = True, ) -> dict[str, Any]: """Restore *backup_path* to *target_path* and re-validate. Before overwriting, the current contents of ``target_path`` (if any) are themselves backed up so this tool is always reversible one more step. If ``validate_after=True``, the restored config is passed through ``opensips -C``; a validation failure does NOT undo the restore (the caller asked for the restore explicitly), but the result is surfaced so the caller can decide what to do next. Parameters ---------- backup_path: Absolute path to the backup file to restore. target_path: Absolute path to overwrite. Must live in an allowlisted dir. validate_after: Run ``opensips -C`` on the restored file. Default ``True``. """ import datetime import shutil if not (os.path.isabs(backup_path) and os.path.isabs(target_path)): return {"error": "backup_path and target_path must both be absolute"} if not os.path.exists(backup_path): return {"error": f"backup_path does not exist: {backup_path}"} if not is_safe_out_dir(os.path.dirname(backup_path)): return {"error": "backup_path is outside the safe dir allowlist"} if not is_safe_out_dir(os.path.dirname(target_path)): return {"error": "target_path is outside the safe dir allowlist"} # Defensive: snapshot the current target so the rollback itself is # reversible. Ignore missing-file — that's fine, nothing to snapshot. pre_rollback_backup: str | None = None if os.path.exists(target_path): ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") pre_rollback_backup = f"{target_path}.pre-rollback-{ts}" try: shutil.copy2(target_path, pre_rollback_backup) except OSError as exc: return {"error": f"pre-rollback snapshot failed: {exc}"} try: shutil.copy2(backup_path, target_path) except OSError as exc: return {"error": f"restore copy failed: {exc}"} out: dict[str, Any] = { "restored_from": backup_path, "restored_to": target_path, "pre_rollback_snapshot": pre_rollback_backup, } if validate_after: try: with open(target_path, encoding="utf-8") as fh: content = fh.read() result = await _validator.validate(content) out["validation"] = { "valid": result.valid, "errors": result.errors, "warnings": result.warnings, } except Exception as exc: out["validation"] = {"error": str(exc)} return out - src/opensips_mcp/tools/cfg_tools.py:1641-1644 (registration)Registered as an MCP tool via @mcp.tool() decorator on line 1641, with @audited('cfg_rollback') and @require_permission('config.write') decorators.
@mcp.tool() @audited("cfg_rollback") @require_permission("config.write") async def cfg_rollback( - src/opensips_mcp/cli.py:322-329 (registration)CLI command registration for cfg_rollback as a Click command under the 'cfg' group. Provides a shell-callable interface that wraps similar logic.
@cfg.command("rollback") @click.argument("backup_path", type=click.Path(exists=True, dir_okay=False)) @click.argument("target_path", type=click.Path()) @click.option("--no-validate", is_flag=True, help="Skip post-rollback opensips -C validation.") def cfg_rollback( backup_path: str, target_path: str, no_validate: bool, ) -> None: