File size: 13,851 Bytes
03cf367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
from __future__ import annotations
import uuid
from typing import Dict, Any, List
from models import Action, ActionType
from tasks.base import BaseTask, InternalState, StepOutput, semantic_match

# Based on real GitHub DNS Outage (January 2016)
# Post-mortem: https://github.blog/news-insights/the-library/dns-outage-post-mortem/
# A Puppet manifest bug restarted only the authoritative nameserver (not the caching one).
# A follow-up re-deploy corrupted the zone file further because the zone rebuild
# script depended on DNS itself β€” which was already broken.

INCIDENT_TIME = "2016-01-28T09:14:22Z"

DEPENDENCIES = [
    {"service": "api-gateway",       "calls": ["dns-caching"],          "called_by": []},
    {"service": "dns-caching",       "calls": ["dns-authoritative"],    "called_by": ["api-gateway"]},
    {"service": "dns-authoritative", "calls": [],                       "called_by": ["dns-caching", "deploy-service"]},
    {"service": "deploy-service",    "calls": ["dns-authoritative"],    "called_by": []},
    {"service": "puppet-master",     "calls": [],                       "called_by": []},
]

DNS_AUTH_LOGS = [
    "[09:14:22] INFO  Puppet run started: nameserver-ip-update manifest",
    "[09:14:45] INFO  Restarting authoritative nameserver... done",
    "[09:14:46] WARN  Caching nameserver NOT restarted (not in manifest scope)",
    "[09:14:47] INFO  Zone transfer initiated to caching nameserver",
    "[09:14:52] ERROR Zone transfer failed: caching nameserver has no new config",
    "[09:15:01] ERROR deploy-service called zone-rebuild API during incident",
    "[09:15:03] ERROR zone-rebuild API made DNS query β†’ got NXDOMAIN β†’ built corrupt zone",
    "[09:15:04] FATAL Zone file now corrupt: 847 records returning NXDOMAIN",
]

DNS_CACHING_LOGS = [
    "[09:14:22] INFO  Serving cached zone: last updated 09:00:00",
    "[09:14:47] WARN  Zone transfer attempted from authoritative β€” rejected (version mismatch)",
    "[09:14:50] WARN  Continuing to serve stale zone (30+ minutes old)",
    "[09:14:55] ERROR api-gateway queries failing: NXDOMAIN for payment.internal",
    "[09:14:57] ERROR api-gateway queries failing: NXDOMAIN for orders.internal",
    "[09:15:10] CRIT  847 DNS queries/min returning NXDOMAIN β€” zone corrupted",
]

DEPLOY_SERVICE_LOGS = [
    "[09:15:00] INFO  Attempting re-deploy to fix DNS issue",
    "[09:15:02] INFO  Calling zone-rebuild API at zone-manager.internal",
    "[09:15:03] ERROR DNS resolution failed for zone-manager.internal: NXDOMAIN",
    "[09:15:03] WARN  Zone rebuild called with null zone data β€” wrote empty zone",
    "[09:15:04] CRIT  Zone rebuild made situation worse β€” do not re-deploy",
]

PUPPET_LOGS = [
    "[09:14:22] INFO  Puppet agent run triggered: nameserver-ip-update",
    "[09:14:45] INFO  Resource File[/etc/named.conf] updated",
    "[09:14:45] INFO  Service[named-authoritative] restarted",
    "[09:14:46] INFO  Puppet run completed successfully",
    "[09:14:46] WARN  Note: named-caching.service not in manifest β€” skipped",
]

API_GATEWAY_LOGS = [
    "[09:14:53] WARN  DNS lookup failed: payment.internal β†’ NXDOMAIN",
    "[09:14:54] WARN  DNS lookup failed: orders.internal β†’ NXDOMAIN",
    "[09:14:55] ERROR 94% of service-discovery lookups returning NXDOMAIN",
    "[09:14:56] CRIT  Upstream services unreachable β€” DNS failure cascade",
]


class DnsTask(BaseTask):
    def initialize(self) -> InternalState:
        logs = {
            "dns-authoritative": DNS_AUTH_LOGS[:],
            "dns-caching":       DNS_CACHING_LOGS[:],
            "deploy-service":    DEPLOY_SERVICE_LOGS[:],
            "puppet-master":     PUPPET_LOGS[:],
            "api-gateway":       API_GATEWAY_LOGS[:],
        }

        services: Dict[str, dict] = {
            "dns-authoritative": {
                "name": "dns-authoritative", "status": "down",
                "cpu_percent": 12.0, "memory_percent": 88.0,
                "error_rate": 847.0, "latency_p99_ms": 0.0,
                "replicas_running": 0, "replicas_desired": 1,
                "current_version": "bind-9.11.3-puppet-r142",
                "last_deployed": "2016-01-28T09:14:45Z",
                "minutes_degraded": 0, "sla_breach": False,
            },
            "dns-caching": {
                "name": "dns-caching", "status": "degraded",
                "cpu_percent": 45.0, "memory_percent": 62.0,
                "error_rate": 94.0, "latency_p99_ms": 5200.0,
                "replicas_running": 1, "replicas_desired": 1,
                "current_version": "unbound-1.5.8",
                "last_deployed": "2016-01-10T00:00:00Z",
                "minutes_degraded": 0, "sla_breach": False,
            },
            "puppet-master": {
                "name": "puppet-master", "status": "healthy",
                "cpu_percent": 22.0, "memory_percent": 35.0,
                "error_rate": 0.0, "latency_p99_ms": 120.0,
                "replicas_running": 1, "replicas_desired": 1,
                "current_version": "puppet-4.3.2-r142",
                "last_deployed": "2016-01-28T09:14:22Z",
                "minutes_degraded": 0, "sla_breach": False,
            },
            "api-gateway": {
                "name": "api-gateway", "status": "down",
                "cpu_percent": 18.0, "memory_percent": 41.0,
                "error_rate": 94.0, "latency_p99_ms": 30000.0,
                "replicas_running": 0, "replicas_desired": 5,
                "current_version": "v2.4.0",
                "last_deployed": "2016-01-20T08:00:00Z",
                "minutes_degraded": 0, "sla_breach": False,
            },
            "deploy-service": {
                "name": "deploy-service", "status": "degraded",
                "cpu_percent": 33.0, "memory_percent": 28.0,
                "error_rate": 12.0, "latency_p99_ms": 8000.0,
                "replicas_running": 1, "replicas_desired": 1,
                "current_version": "v1.8.0",
                "last_deployed": "2016-01-28T09:15:00Z",
                "minutes_degraded": 0, "sla_breach": False,
            },
        }

        alerts = [
            {
                "id": "A001", "severity": "critical", "service": "api-gateway",
                "message": "94% DNS queries returning NXDOMAIN β€” services unreachable",
                "timestamp": "2016-01-28T09:14:55Z", "acknowledged": False,
            },
            {
                "id": "A002", "severity": "critical", "service": "dns-authoritative",
                "message": "Zone file corrupted β€” 847 records returning NXDOMAIN",
                "timestamp": "2016-01-28T09:15:04Z", "acknowledged": False,
            },
            {
                "id": "A003", "severity": "warning", "service": "dns-caching",
                "message": "Serving stale zone β€” zone transfer rejected (version mismatch)",
                "timestamp": "2016-01-28T09:14:47Z", "acknowledged": False,
            },
            {
                "id": "A004", "severity": "info", "service": "puppet-master",
                "message": "Puppet run completed successfully (manifest bug already applied)",
                "timestamp": "2016-01-28T09:14:46Z", "acknowledged": False,
            },
        ]

        state = InternalState(
            episode_id=str(uuid.uuid4()),
            task_id="dns",
            step=0,
            max_steps=20,
            services=services,
            alerts=alerts,
            logs=logs,
            action_history=[],
            total_reward=0.0,
            incident_resolved=False,
            ground_truth_root_cause="puppet_manifest_bug_restarted_wrong_nameserver_corrupt_dns_zone",
            ground_truth_fix="restart dns-caching nameserver AND rollback puppet manifest",
            incident_start_time=INCIDENT_TIME,
            healthy_services=["puppet-master"],
            service_dependencies=DEPENDENCIES,
        )
        return state

    def step(self, state: InternalState, action: Action) -> StepOutput:
        state.step += 1
        state._apply_sla_degradation()
        at = action.action_type
        svc = action.service or ""
        reward = 0.0
        done = False
        info: Dict[str, Any] = {}

        result_text, error_text = self._apply_action_to_logs(state, action)

        # --- Information-gathering rewards ---
        gather_map = {
            ("read_logs",   "dns-authoritative"): ("rl_dns_auth",  0.10),
            ("search_logs", "dns-authoritative"): ("rl_dns_auth",  0.10),
            ("read_logs",   "dns-caching"):        ("rl_dns_cache", 0.10),
            ("search_logs", "dns-caching"):        ("rl_dns_cache", 0.10),
            ("read_logs",   "deploy-service"):     ("rl_deploy",    0.05),
            ("search_logs", "deploy-service"):     ("rl_deploy",    0.05),
        }
        k = (at.value, svc)
        if k in gather_map:
            tag, r = gather_map[k]
            if tag not in state.rewards_given:
                reward += r
                state.rewards_given.add(tag)

        if at == ActionType.READ_RUNBOOK:
            if "runbook" not in state.rewards_given:
                reward += 0.05
                state.rewards_given.add("runbook")

        # --- Diagnose ---
        if at == ActionType.DIAGNOSE:
            rc = action.root_cause or ""
            dns_keywords = ["puppet", "manifest", "nameserver", "dns", "zone", "nxdomain", "caching", "corrupt"]
            if semantic_match(rc, dns_keywords, threshold=1):
                if "diagnose_correct" not in state.rewards_given:
                    reward += 0.20
                    state.rewards_given.add("diagnose_correct")
            result_text = f"Diagnosis recorded: {rc}"

        # --- Fix 1: Restart dns-caching (primary fix β€” restarts with new config) ---
        if at == ActionType.RESTART_SERVICE and svc == "dns-caching":
            blind_penalty = self._penalty_blind_remediation(state, action, "fix_cache_restart")
            reward += blind_penalty
            if "fix_cache_restart" not in state.rewards_given:
                reward += 0.25
                state.rewards_given.add("fix_cache_restart")
                state.services["dns-caching"]["status"] = "healthy"
                state.services["dns-caching"]["error_rate"] = 0.0
                state.services["dns-caching"]["latency_p99_ms"] = 5.0
                state.services["api-gateway"]["error_rate"] = 0.0
                state.services["api-gateway"]["status"] = "healthy"
                state.services["api-gateway"]["replicas_running"] = 5
                state.alerts = [a for a in state.alerts if a["id"] not in ("A002", "A003")]
                result_text = (
                    "dns-caching restarted. Loaded fresh config from authoritative nameserver. "
                    "Zone transfer successful. DNS resolution restored. "
                    "api-gateway recovering β€” NXDOMAIN rate dropping."
                )
                if "fix_puppet_rollback" in state.rewards_given:
                    state.incident_resolved = True
                    done = True
                    info["resolution"] = "incident_resolved"

        # --- Fix 2: Rollback puppet-master (prevents recurrence) ---
        if at == ActionType.ROLLBACK and svc == "puppet-master":
            if "fix_puppet_rollback" not in state.rewards_given:
                reward += 0.15
                state.rewards_given.add("fix_puppet_rollback")
                state.services["puppet-master"]["current_version"] = "puppet-4.3.2-r141"
                result_text = (
                    "Puppet manifest rolled back to r141. "
                    "The nameserver-ip-update manifest has been reverted. "
                    "Future Puppet runs will restart both authoritative AND caching nameservers."
                )
                if "fix_cache_restart" in state.rewards_given:
                    state.incident_resolved = True
                    done = True
                    info["resolution"] = "incident_resolved"

        # --- Penalty: Running deploy-service makes it worse ---
        if at == ActionType.RESTART_SERVICE and svc == "deploy-service":
            reward -= 0.20
            result_text = (
                "CRITICAL: Re-running deploy-service triggered another zone rebuild attempt. "
                "zone-rebuild API made DNS query β†’ got NXDOMAIN β†’ zone corruption worsened. "
                "DO NOT re-deploy during a DNS incident β€” the deploy script depends on DNS."
            )

        # Penalty: restarting healthy puppet-master without diagnosis
        if at == ActionType.RESTART_SERVICE and svc == "puppet-master":
            reward -= 0.05
            result_text = "puppet-master restarted. The manifest bug is still present β€” use rollback instead."

        # Penalty: collateral damage on healthy services
        if at == ActionType.RESTART_SERVICE and svc in state.healthy_services:
            reward -= 0.10
            error_text = f"Collateral damage: {svc} was healthy. Unnecessary restart."

        if at == ActionType.NOOP and state.step > 4:
            reward -= 0.04

        if at in (ActionType.BLOCK_IP_RANGE, ActionType.CREATE_INDEX, ActionType.FAILOVER):
            reward -= 0.10
            error_text = f"Action {at.value} is not applicable to a DNS configuration incident."

        state.total_reward = self._clamp(state.total_reward + reward)
        if state.step >= state.max_steps and not done:
            done = True
            info["reason"] = "max_steps_reached"

        obs = state._build_observation(last_action_result=result_text, last_action_error=error_text)
        state.action_history.append({"step": state.step, "action": action.model_dump(), "reward": round(reward, 4)})
        return StepOutput(next_state=state, reward=round(reward, 4), done=done, info=info)