← Change Manager: risco, autorização e coordenação
07 / 10 · 60 MIN

Modelos, risco agregado e calendário

Relaciona elegibilidade, dependências e capacidade validada antes de coordenar a execução.

Confirmar as condições do modelo

Num exercício fictício, um modelo pré-autorizado permite atualizar até dois nós, sequencialmente, com redundância saudável. Um pedido de dois nós em paralelo respeita a quantidade, mas falha a sequência. Se um nó já estiver indisponível, também falha a condição de redundância. A análise deve comparar todas as condições relevantes com o estado atual. Vinte execuções bem-sucedidas em teste não demonstram aplicabilidade a outra topologia de produção. Pede a versão do modelo, evidência das condições e tratamento previsto para desvios. Uma resposta possível é reformular o pedido para satisfazer o modelo; outra é encaminhar a exceção para quem tem mandato para a decidir. O Change Manager pode coordenar a preparação sem se atribuir a aceitação de todos os riscos. A frequência da tarefa e a familiaridade da equipa ajudam a investigação, mas não substituem as condições acordadas.

Analisar o serviço e a recuperação partilhados

A mudança A retira o nó primário e B retira o secundário. Cada pedido isolado afirma que o outro nó mantém o serviço. Os identificadores de infraestrutura são distintos, mas o pressuposto de continuidade é comum. Pede um desenho simples com serviço, nós, dependências de recuperação e intervalos de exposição. Dois servidores de aplicações diferentes também podem depender do mesmo gateway para recuperar ficheiros. Se a manutenção desse gateway coincidir com a outra intervenção, a recuperação prevista pode deixar de estar disponível. A ausência de identificadores repetidos numa folha não prova independência. O calendário depende da qualidade das relações fornecidas. No trabalho real, confirma essas relações com os responsáveis técnicos e de serviço. Se uma mudança pode ser adiada e outra tem um prazo confirmado, compara uma sequência que preserve a capacidade necessária, incluindo a validação entre intervenções.

Reservar tempo até à capacidade validada

N1 fica indisponível entre 01:00 e 01:25 UTC e N2 entre 01:15 e 01:40 UTC. Há dez minutos de sobreposição, entre 01:15 e 01:25. Não são quarenta minutos, que representam a extensão total dos dois intervalos. Agora considera outra proposta: o comando em N1 termina às 01:25, mas a validação necessária termina às 01:35; B retira N2 às 01:25. Os comandos não se sobrepõem, mas durante dez minutos ainda não existe a capacidade validada exigida. Representa esse intervalo no plano e confirma se B pode começar depois da validação. Num cálculo separado, quarenta minutos de execução seguidos de quinze de validação, com conclusão às 02:00, exigem início até 01:05 sem margem. Estes números são condições pedagógicas, não janelas ou políticas BNP Paribas. Estimativas, recuperação e contingência precisam de análise adicional no contexto real.

Executar e criticar o modelo local

Guarda o código completo abaixo como change-control.py e executa python3 change-control.py. O programa usa apenas a biblioteca padrão e imprime treze grupos de verificações. Foi executado com CPython 3.13.1. Os intervalos usam minutos do mesmo dia UTC; neste modelo, o fim não pertence ao intervalo. Não existe conversão de fusos, passagem da meia-noite ou validação geral de entradas. Observa a diferença entre intervalos de comandos e de exposição. Depois apaga as dependências declaradas de B: o resultado deixa de mostrar recursos comuns, embora o programa não tenha descoberto nada sobre a infraestrutura. Experimenta mudar o serviço de S para T mantendo a mesma decisão; tempo restante não amplia o âmbito. Explica por escrito o que cada resultado demonstra e a informação ainda necessária. Este código não verifica identidade, autoridade humana, configuração GitHub/GitLab ou autorização real de produção.

"""Original DR review aid with fictional inputs, not an authorization engine."""
from fractions import Fraction
import hashlib
import json
import platform
from pathlib import Path


def overlap(left, right):
    """Integer minutes in the same UTC day; half-open intervals in this exercise."""
    if any(start > end for start, end in [left, right]):
        raise ValueError("Invalid interval")
    return max(0, min(left[1], right[1]) - max(left[0], right[0]))


def shared_exposure(a, b):
    duration = overlap(a["exposure"], b["exposure"])
    return {"minutes": duration, "services": sorted(set(a["services"]) & set(b["services"])),
            "recoveryDependencies": sorted(set(a["recoveryDependencies"]) & set(b["recoveryDependencies"]))}


def model_gaps(request):
    gaps = []
    if request["nodes"] > 2:
        gaps.append("node-count")
    if request["mode"] != "sequential":
        gaps.append("sequence")
    if not request["healthyRedundancy"]:
        gaps.append("redundancy")
    return gaps


def decision_gaps(decision, request, now):
    # All identities, authority, observations and times are supplied, not verified.
    gaps = []
    if decision["state"] != "approved":
        gaps.append("decision-not-active")
    if request["service"] not in decision["services"]:
        gaps.append("scope")
    if not decision["start"] <= now < decision["end"]:
        gaps.append("time")
    return gaps


def config_diff(proposed, actual):
    keys = proposed.keys() | actual.keys()
    return sorted(k for k in keys if k not in proposed or k not in actual or proposed[k] != actual[k])


def run():
    checks = []

    def check(name, actual, expected):
        if actual != expected:
            raise AssertionError((name, actual, expected))
        checks.append({"name": name, "actual": actual, "passed": True})

    check("two_nodes_unavailable_together", overlap((60, 85), (75, 100)), 10)
    check("command_intervals_touch_but_validation_extends_exposure",
          {"commands": overlap((60, 85), (85, 110)), "exposure": overlap((60, 95), (85, 110))},
          {"commands": 0, "exposure": 10})
    check("replanned_second_node_starts_after_validation", overlap((60, 95), (95, 120)), 0)
    a = {"exposure": (60, 95), "services": ["funds"], "recoveryDependencies": ["gateway-G"]}
    b = {"exposure": (85, 110), "services": ["funds"], "recoveryDependencies": ["gateway-G"]}
    check("distinct_nodes_can_share_service_and_recovery", shared_exposure(a, b),
          {"minutes": 10, "services": ["funds"], "recoveryDependencies": ["gateway-G"]})
    omitted = {**b, "services": [], "recoveryDependencies": []}
    check("omitted_dependencies_are_not_discovered", shared_exposure(a, omitted),
          {"minutes": 10, "services": [], "recoveryDependencies": []})
    check("latest_start_includes_validation", 120 - 40 - 15, 65)  # 01:05 UTC
    request = {"nodes": 2, "mode": "parallel", "healthyRedundancy": False}
    check("count_alone_does_not_satisfy_model", model_gaps(request), ["sequence", "redundancy"])
    check("matching_model_fields_only", model_gaps({**request, "mode": "sequential", "healthyRedundancy": True}), [])
    decision = {"state": "approved", "services": ["S"], "start": 120, "end": 180}
    check("remaining_time_does_not_expand_scope", decision_gaps(decision, {"service": "T"}, 170), ["scope"])
    withdrawn = {**decision, "state": "withdrawn"}
    check("withdrawal_overrides_old_display_label", decision_gaps(withdrawn, {"service": "S"}, 175), ["decision-not-active"])
    check("fictional_expiry_boundary_is_exclusive",
          [decision_gaps(decision, {"service": "S"}, t) for t in [179, 180]], [[], ["time"]])
    proposed = {"binary": "R4", "access": "restricted", "pool": 8}
    actual = {**proposed, "access": "expanded"}
    check("successful_process_does_not_erase_config_deviation", config_diff(proposed, actual), ["access"])
    requests, canceled, implemented, failed = 50, 20, 30, 3
    check("failure_rate_uses_defined_implemented_population",
          {"requested": requests, "canceled": canceled, "implemented": implemented,
           "failureRate": str(Fraction(failed, implemented)), "incorrectRequestRate": str(Fraction(failed, requests))},
          {"requested": 50, "canceled": 20, "implemented": 30, "failureRate": "1/10", "incorrectRequestRate": "3/50"})
    return {"scope": "Fictional calendar, eligibility and record comparisons only. No real authority, GitHub or GitLab configuration, deployment, identity authentication or production permission is verified. Empty gap lists establish only supplied model conditions; omitted dependencies remain unknown.",
            "python": platform.python_version(), "scriptSha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
            "groups": len(checks), "checks": checks}


if __name__ == "__main__":
    print(json.dumps(run(), ensure_ascii=False, indent=2))
NA PRÁTICA

A e B têm servidores distintos, mas ambas dependem do gateway G. Uma lista de nós sem interseção não responde à pergunta sobre recuperação.

Armadilhas comuns

Contar repetições como elegibilidade; ignorar dependências comuns; usar fim do comando como capacidade disponível; tratar entradas incompletas como prova de independência.

Tópicos relacionados: Risco agregado · Resiliência técnica · Planeamento de mudanças

Leva esta ideia contigo

Coordena o intervalo em que a capacidade fica exposta, com condições do modelo e dependências explícitas.

Criar conta

Referência: Guide for Security-Focused Configuration Management of Information Systems · NIST SP 800-128 updated October 2019; DORA five-metric model and change approval guidance; vendor documentation inspected 2026-10-01