geenforce: refuse to publish a manifest the fleet's lib cannot read
The engine treats a minor-newer manifest as backward compatible and carries on. That holds for additions which WIDEN behaviour - an old lib skips a Type it does not know - and inverts for one that NARROWS it. _CmmVersion arrived in lib 2.6 as a minor bump, so a PC on 2.5 does not recognise the field, reads every gated entry as unfiltered, and installs every PC-DMIS version it cannot detect, on every CMM, within one cycle. The share runbook already says push the lib first. A runbook is not a control, and the failure is silent, fleet-wide and about five minutes fast. ShopDB already had the evidence and was not using it: every enforcement report carries the enforcer version, and publish_scope had no gate at all. It now compares the scope's manifest version against the versions PCs actually report for that scope and refuses when any is behind, naming the hosts. force=True for someone who knows why. A report with no or an unreadable version counts as behind - that field arrived with the summary-emitting engine, so its absence IS an old lib, and treating unknown as safe is precisely how this fails open. A scope nobody has reported for still publishes, or a fresh site could never publish anything. Versions compare numerically, since as text '2.10' sorts below '2.9'. Also exposed as a preflight endpoint so the UI can warn before someone clicks publish, and as a 409 with the offending hosts rather than a 500.
This commit is contained in:
@@ -726,17 +726,49 @@ def publish_scope_route(scopeid):
|
||||
if not scope:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'No such scope', http_code=404)
|
||||
from flask_jwt_extended import get_jwt_identity
|
||||
notes = (request.get_json(silent=True) or {}).get('notes')
|
||||
body = request.get_json(silent=True) or {}
|
||||
notes = body.get('notes')
|
||||
try:
|
||||
publishedby = int(get_jwt_identity())
|
||||
except (TypeError, ValueError):
|
||||
publishedby = None
|
||||
version = service.publish_scope(scope.scopename, scope.phase,
|
||||
notes=notes, publishedby=publishedby)
|
||||
try:
|
||||
version = service.publish_scope(
|
||||
scope.scopename, scope.phase, notes=notes,
|
||||
publishedby=publishedby, force=bool(body.get('force')))
|
||||
except service.LibVersionTooOldError as error:
|
||||
# 409, not 500: the request is well-formed, the fleet is not ready. The
|
||||
# hosts come back so the caller can act without going hunting.
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT, str(error), http_code=409,
|
||||
details={'required': error.required, 'floor': error.floor,
|
||||
'hosts': error.hosts})
|
||||
db.session.commit()
|
||||
return success_response({'versionnumber': version}, http_code=201)
|
||||
|
||||
|
||||
@geenforce_bp.route('/scopes/<int:scopeid>/publish-preflight', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.manage')
|
||||
def publish_preflight(scopeid):
|
||||
"""Would publishing this scope outrun the fleet's enforcer lib?
|
||||
|
||||
Lets the UI warn BEFORE someone clicks publish, rather than only refusing
|
||||
afterwards.
|
||||
"""
|
||||
scope = db.session.get(ManifestScope, scopeid)
|
||||
if not scope:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'No such scope', http_code=404)
|
||||
hosts, floor = service.hosts_below_libversion(
|
||||
scope.scopename, scope.phase, scope.manifestversion)
|
||||
return success_response({
|
||||
'required': scope.manifestversion,
|
||||
'floor': floor,
|
||||
'hostsbehind': hosts,
|
||||
'canpublish': not hosts,
|
||||
})
|
||||
|
||||
|
||||
@geenforce_bp.route('/scopes/<int:scopeid>/versions', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.manage')
|
||||
|
||||
Reference in New Issue
Block a user