summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJohn Estabrook <jestabro@vyos.io>2025-05-28 19:49:31 -0500
committerJohn Estabrook <jestabro@vyos.io>2025-05-29 22:09:24 -0500
commitb73fc84c4ab50a4007ecdddc9417e2012d2ea11a (patch)
treefa99f70275e557aad2caf0d24049bd5b9f9c3f79 /src
parent08dc2e56bf4ad487709ec3849fe97ec24d5b35fe (diff)
downloadvyos-1x-b73fc84c4ab50a4007ecdddc9417e2012d2ea11a.tar.gz
vyos-1x-b73fc84c4ab50a4007ecdddc9417e2012d2ea11a.zip
http-api: T3955: add commit-confirm to endpoints /configure /config-file
Diffstat (limited to 'src')
-rw-r--r--src/services/api/rest/models.py10
-rw-r--r--src/services/api/rest/routers.py76
2 files changed, 76 insertions, 10 deletions
diff --git a/src/services/api/rest/models.py b/src/services/api/rest/models.py
index 47c7a65b3..c5cb4af48 100644
--- a/src/services/api/rest/models.py
+++ b/src/services/api/rest/models.py
@@ -26,6 +26,7 @@ from typing import Self
from pydantic import BaseModel
from pydantic import StrictStr
+from pydantic import StrictInt
from pydantic import field_validator
from pydantic import model_validator
from fastapi.responses import HTMLResponse
@@ -71,6 +72,8 @@ class BaseConfigureModel(BasePathModel):
class ConfigureModel(ApiModel, BaseConfigureModel):
+ confirm_time: StrictInt = 0
+
class Config:
json_schema_extra = {
'example': {
@@ -81,8 +84,12 @@ class ConfigureModel(ApiModel, BaseConfigureModel):
}
+class ConfirmModel(ApiModel):
+ op: StrictStr
+
class ConfigureListModel(ApiModel):
commands: List[BaseConfigureModel]
+ confirm_time: StrictInt = 0
class Config:
json_schema_extra = {
@@ -135,12 +142,13 @@ class ConfigFileModel(ApiModel):
op: StrictStr
file: StrictStr = None
string: StrictStr = None
+ confirm_time: StrictInt = 0
class Config:
json_schema_extra = {
'example': {
'key': 'id_key',
- 'op': 'save | load | merge',
+ 'op': 'save | load | merge | confirm',
'file': 'filename',
'string': 'config_string'
}
diff --git a/src/services/api/rest/routers.py b/src/services/api/rest/routers.py
index 4866ec5d8..a2e6b4178 100644
--- a/src/services/api/rest/routers.py
+++ b/src/services/api/rest/routers.py
@@ -51,6 +51,7 @@ from .models import error
from .models import responses
from .models import ApiModel
from .models import ConfigureModel
+from .models import ConfirmModel
from .models import ConfigureListModel
from .models import ConfigSectionModel
from .models import ConfigSectionListModel
@@ -302,8 +303,24 @@ def call_commit(s: SessionState):
LOG.warning(f'ConfigSessionError: {e}')
+def call_commit_confirm(s: SessionState):
+ env = s.session.get_session_env()
+ env['IN_COMMIT_CONFIRM'] = 't'
+ try:
+ s.session.commit()
+ except ConfigSessionError as e:
+ s.session.discard()
+ if s.debug:
+ LOG.warning(f'ConfigSessionError:\n {traceback.format_exc()}')
+ else:
+ LOG.warning(f'ConfigSessionError: {e}')
+ finally:
+ del env['IN_COMMIT_CONFIRM']
+
+
def _configure_op(
data: Union[
+ ConfirmModel,
ConfigureModel,
ConfigureListModel,
ConfigSectionModel,
@@ -320,6 +337,11 @@ def _configure_op(
session = state.session
env = session.get_session_env()
+ # A non-zero confirm_time will start commit-confirm timer on commit
+ confirm_time = 0
+ if isinstance(data, (ConfigureModel, ConfigureListModel, ConfigFileModel)):
+ confirm_time = data.confirm_time
+
# Allow users to pass just one command
if not isinstance(data, (ConfigureListModel, ConfigSectionListModel)):
data = [data]
@@ -339,10 +361,16 @@ def _configure_op(
try:
for c in data:
op = c.op
- if not isinstance(c, BaseConfigSectionTreeModel):
+ if not isinstance(c, (ConfirmModel, BaseConfigSectionTreeModel)):
path = c.path
- if isinstance(c, BaseConfigureModel):
+ if isinstance(c, ConfirmModel):
+ if op == 'confirm':
+ msg = session.confirm()
+ else:
+ raise ConfigSessionError(f"'{op}' is not a valid operation")
+
+ elif isinstance(c, BaseConfigureModel):
if c.value:
value = c.value
else:
@@ -388,16 +416,26 @@ def _configure_op(
else:
raise ConfigSessionError(f"'{op}' is not a valid operation")
# end for
+
config = Config(session_env=env)
d = get_config_diff(config)
+ if confirm_time:
+ out = session.commit_confirm(minutes=confirm_time)
+ msg = msg + out if msg else out
+ env['IN_COMMIT_CONFIRM'] = 't'
+
if d.is_node_changed(['service', 'https']):
- background_tasks.add_task(call_commit, state)
- msg = self_ref_msg
+ if confirm_time:
+ background_tasks.add_task(call_commit_confirm, state)
+ else:
+ background_tasks.add_task(call_commit, state)
+ out = self_ref_msg
+ msg = msg + out if msg else out
else:
# capture non-fatal warnings
out = session.commit()
- msg = out if out else msg
+ msg = msg + out if msg else out
LOG.info(f"Configuration modified via HTTP API using key '{state.id}'")
except ConfigSessionError as e:
@@ -414,6 +452,8 @@ def _configure_op(
# Don't give the details away to the outer world
error_msg = 'An internal error occured. Check the logs for details.'
finally:
+ if 'IN_COMMIT_CONFIRM' in env:
+ del env['IN_COMMIT_CONFIRM']
lock.release()
if status != 200:
@@ -433,7 +473,7 @@ def create_path_import_pki_no_prompt(path):
@router.post('/configure')
def configure_op(
- data: Union[ConfigureModel, ConfigureListModel],
+ data: Union[ConfigureModel, ConfigureListModel, ConfirmModel],
request: Request,
background_tasks: BackgroundTasks,
):
@@ -501,6 +541,8 @@ def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTasks):
op = data.op
msg = None
+ lock.acquire()
+
try:
if op == 'save':
if data.file:
@@ -527,11 +569,23 @@ def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTasks):
config = Config(session_env=env)
d = get_config_diff(config)
+ if data.confirm_time:
+ out = session.commit_confirm(minutes=data.confirm_time)
+ msg = msg + out if msg else out
+ env['IN_COMMIT_CONFIRM'] = 't'
+
if d.is_node_changed(['service', 'https']):
- background_tasks.add_task(call_commit, state)
- msg = self_ref_msg
+ if data.confirm_time:
+ background_tasks.add_task(call_commit_confirm, state)
+ else:
+ background_tasks.add_task(call_commit, state)
+ out = self_ref_msg
+ msg = msg + out if msg else out
else:
- session.commit()
+ out = session.commit()
+ msg = msg + out if msg else out
+ elif op == 'confirm':
+ msg = session.confirm()
else:
return error(400, f"'{op}' is not a valid operation")
except ConfigSessionError as e:
@@ -539,6 +593,10 @@ def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTasks):
except Exception:
LOG.critical(traceback.format_exc())
return error(500, 'An internal error occured. Check the logs for details.')
+ finally:
+ if 'IN_COMMIT_CONFIRM' in env:
+ del env['IN_COMMIT_CONFIRM']
+ lock.release()
return success(msg)