redeploy 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: redeploy
3
+ Version: 0.1.1
4
+ Summary: Infrastructure migration toolkit: detect → plan → apply
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pydantic>=2.0
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: click>=8.0
10
+ Requires-Dist: loguru>=0.7
11
+ Requires-Dist: paramiko>=3.0
12
+ Requires-Dist: httpx>=0.25
13
+ Requires-Dist: rich>=13.0
14
+ Requires-Dist: goal>=2.1.0
15
+ Requires-Dist: costs>=0.1.20
16
+ Requires-Dist: pfix>=0.1.60
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0; extra == "dev"
19
+ Requires-Dist: ruff>=0.4; extra == "dev"
20
+ Requires-Dist: goal>=2.1.0; extra == "dev"
21
+ Requires-Dist: costs>=0.1.20; extra == "dev"
22
+ Requires-Dist: pfix>=0.1.60; extra == "dev"
23
+
24
+ # redeploy
25
+
26
+ ## AI Cost Tracking
27
+
28
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.15-brightgreen) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
29
+
30
+ This project uses AI-generated code. Total cost: **$0.1500** with **1** AI commits.
31
+
32
+ Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
33
+
34
+ ---
35
+
36
+
37
+
38
+ Infrastructure migration toolkit z 3 funkcjami:
39
+
40
+ ```
41
+ redeploy detect → infra.yaml (co jest teraz)
42
+ redeploy plan → migration-plan.yaml (co zrobić)
43
+ redeploy apply → wykonanie planu (wykonaj migrację)
44
+ ```
45
+
46
+ ## Instalacja
47
+
48
+ ```bash
49
+ pip install -e .
50
+ ```
51
+
52
+ ## Użycie
53
+
54
+ ```bash
55
+ # 1. Wykryj aktualną infrastrukturę (lokalną lub zdalną)
56
+ redeploy detect --host root@87.106.87.183 --app c2004 -o infra.yaml
57
+
58
+ # 2. Zaplanuj migrację (np. z k3s → docker_full)
59
+ redeploy plan --infra infra.yaml --target target.yaml -o migration-plan.yaml
60
+
61
+ # 3. Wykonaj plan
62
+ redeploy apply --plan migration-plan.yaml
63
+
64
+ # Lub wszystko naraz (detect → plan → apply)
65
+ redeploy migrate --host root@87.106.87.183 --app c2004 --target target.yaml
66
+ ```
67
+
68
+ ## Funkcje
69
+
70
+ ### `detect` — Skanowanie infrastruktury
71
+
72
+ Wykrywa aktualny stan:
73
+ - Zdalny host: SSH, Docker, k3s, systemd, Podman, porty
74
+ - Usługi: działające kontenery, pody k8s/k3s, serwisy systemd
75
+ - Sieć: iptables, nginx, Traefik, zajęte porty
76
+ - Aplikacja: wersja, healthcheck, DB
77
+ - Wyjście: `infra.yaml`
78
+
79
+ ### `plan` — Planowanie migracji
80
+
81
+ Na podstawie `infra.yaml` (stan obecny) i `target.yaml` (cel):
82
+ - Identyfikuje konflikty (porty, stare serwisy)
83
+ - Generuje sekwencję kroków migracji
84
+ - Szacuje ryzyko i downtime
85
+ - Wyjście: `migration-plan.yaml`
86
+
87
+ ### `apply` — Wykonanie migracji
88
+
89
+ Wykonuje plan krok po kroku:
90
+ - Dry-run przed wykonaniem
91
+ - Rollback przy błędzie
92
+ - Verify po każdym etapie
93
+ - Log wszystkich operacji
94
+
95
+ ## Format plików
96
+
97
+ ### `infra.yaml` (wynik detect)
98
+
99
+ ```yaml
100
+ host: root@87.106.87.183
101
+ app: c2004
102
+ scanned_at: "2026-04-20T13:00:00"
103
+ runtime:
104
+ docker: "27.0.3"
105
+ k3s: "v1.31.5+k3s1" # lub null
106
+ podman: null
107
+ systemd: "257"
108
+ ports:
109
+ 80: { process: docker-proxy, via: traefik }
110
+ 443: { process: docker-proxy, via: traefik }
111
+ services:
112
+ docker:
113
+ - name: c2004-backend
114
+ image: c2004-backend
115
+ status: healthy
116
+ version: "1.0.19"
117
+ ports: [8000]
118
+ k3s:
119
+ - namespace: identification
120
+ name: backend
121
+ status: running
122
+ version: "1.0.18"
123
+ conflicts:
124
+ - type: port_steal
125
+ description: "k3s iptables DNAT on :443 before docker-proxy"
126
+ severity: high
127
+ ```
128
+
129
+ ### `target.yaml` (cel migracji)
130
+
131
+ ```yaml
132
+ strategy: docker_full
133
+ app: c2004
134
+ compose_files:
135
+ - docker-compose.vps.yml
136
+ env_file: envs/vps.env
137
+ stop_services:
138
+ - k3s
139
+ - nginx
140
+ ```
141
+
142
+ ### `migration-plan.yaml` (wynik plan)
143
+
144
+ ```yaml
145
+ from_infra: infra.yaml
146
+ target: target.yaml
147
+ created_at: "2026-04-20T13:05:00"
148
+ risk: medium
149
+ estimated_downtime: "30s"
150
+ steps:
151
+ - id: stop_k3s
152
+ action: systemctl_stop
153
+ service: k3s
154
+ reason: "k3s steals iptables DNAT on :443"
155
+ - id: deploy_docker
156
+ action: docker_compose_up
157
+ compose: docker-compose.vps.yml
158
+ flags: [--build, -d]
159
+ - id: verify
160
+ action: http_check
161
+ url: https://c2004.mask.services/api/v1/health
162
+ expect: "1.0.19"
163
+ ```
164
+
165
+
166
+ ## License
167
+
168
+ Licensed under Apache-2.0.
@@ -0,0 +1,145 @@
1
+ # redeploy
2
+
3
+ ## AI Cost Tracking
4
+
5
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.15-brightgreen) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
6
+
7
+ This project uses AI-generated code. Total cost: **$0.1500** with **1** AI commits.
8
+
9
+ Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
10
+
11
+ ---
12
+
13
+
14
+
15
+ Infrastructure migration toolkit z 3 funkcjami:
16
+
17
+ ```
18
+ redeploy detect → infra.yaml (co jest teraz)
19
+ redeploy plan → migration-plan.yaml (co zrobić)
20
+ redeploy apply → wykonanie planu (wykonaj migrację)
21
+ ```
22
+
23
+ ## Instalacja
24
+
25
+ ```bash
26
+ pip install -e .
27
+ ```
28
+
29
+ ## Użycie
30
+
31
+ ```bash
32
+ # 1. Wykryj aktualną infrastrukturę (lokalną lub zdalną)
33
+ redeploy detect --host root@87.106.87.183 --app c2004 -o infra.yaml
34
+
35
+ # 2. Zaplanuj migrację (np. z k3s → docker_full)
36
+ redeploy plan --infra infra.yaml --target target.yaml -o migration-plan.yaml
37
+
38
+ # 3. Wykonaj plan
39
+ redeploy apply --plan migration-plan.yaml
40
+
41
+ # Lub wszystko naraz (detect → plan → apply)
42
+ redeploy migrate --host root@87.106.87.183 --app c2004 --target target.yaml
43
+ ```
44
+
45
+ ## Funkcje
46
+
47
+ ### `detect` — Skanowanie infrastruktury
48
+
49
+ Wykrywa aktualny stan:
50
+ - Zdalny host: SSH, Docker, k3s, systemd, Podman, porty
51
+ - Usługi: działające kontenery, pody k8s/k3s, serwisy systemd
52
+ - Sieć: iptables, nginx, Traefik, zajęte porty
53
+ - Aplikacja: wersja, healthcheck, DB
54
+ - Wyjście: `infra.yaml`
55
+
56
+ ### `plan` — Planowanie migracji
57
+
58
+ Na podstawie `infra.yaml` (stan obecny) i `target.yaml` (cel):
59
+ - Identyfikuje konflikty (porty, stare serwisy)
60
+ - Generuje sekwencję kroków migracji
61
+ - Szacuje ryzyko i downtime
62
+ - Wyjście: `migration-plan.yaml`
63
+
64
+ ### `apply` — Wykonanie migracji
65
+
66
+ Wykonuje plan krok po kroku:
67
+ - Dry-run przed wykonaniem
68
+ - Rollback przy błędzie
69
+ - Verify po każdym etapie
70
+ - Log wszystkich operacji
71
+
72
+ ## Format plików
73
+
74
+ ### `infra.yaml` (wynik detect)
75
+
76
+ ```yaml
77
+ host: root@87.106.87.183
78
+ app: c2004
79
+ scanned_at: "2026-04-20T13:00:00"
80
+ runtime:
81
+ docker: "27.0.3"
82
+ k3s: "v1.31.5+k3s1" # lub null
83
+ podman: null
84
+ systemd: "257"
85
+ ports:
86
+ 80: { process: docker-proxy, via: traefik }
87
+ 443: { process: docker-proxy, via: traefik }
88
+ services:
89
+ docker:
90
+ - name: c2004-backend
91
+ image: c2004-backend
92
+ status: healthy
93
+ version: "1.0.19"
94
+ ports: [8000]
95
+ k3s:
96
+ - namespace: identification
97
+ name: backend
98
+ status: running
99
+ version: "1.0.18"
100
+ conflicts:
101
+ - type: port_steal
102
+ description: "k3s iptables DNAT on :443 before docker-proxy"
103
+ severity: high
104
+ ```
105
+
106
+ ### `target.yaml` (cel migracji)
107
+
108
+ ```yaml
109
+ strategy: docker_full
110
+ app: c2004
111
+ compose_files:
112
+ - docker-compose.vps.yml
113
+ env_file: envs/vps.env
114
+ stop_services:
115
+ - k3s
116
+ - nginx
117
+ ```
118
+
119
+ ### `migration-plan.yaml` (wynik plan)
120
+
121
+ ```yaml
122
+ from_infra: infra.yaml
123
+ target: target.yaml
124
+ created_at: "2026-04-20T13:05:00"
125
+ risk: medium
126
+ estimated_downtime: "30s"
127
+ steps:
128
+ - id: stop_k3s
129
+ action: systemctl_stop
130
+ service: k3s
131
+ reason: "k3s steals iptables DNAT on :443"
132
+ - id: deploy_docker
133
+ action: docker_compose_up
134
+ compose: docker-compose.vps.yml
135
+ flags: [--build, -d]
136
+ - id: verify
137
+ action: http_check
138
+ url: https://c2004.mask.services/api/v1/health
139
+ expect: "1.0.19"
140
+ ```
141
+
142
+
143
+ ## License
144
+
145
+ Licensed under Apache-2.0.
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "redeploy"
7
+ version = "0.1.1"
8
+ description = "Infrastructure migration toolkit: detect → plan → apply"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "pydantic>=2.0",
13
+ "pyyaml>=6.0",
14
+ "click>=8.0",
15
+ "loguru>=0.7",
16
+ "paramiko>=3.0",
17
+ "httpx>=0.25",
18
+ "rich>=13.0",
19
+ "goal>=2.1.0",
20
+ "costs>=0.1.20",
21
+ "pfix>=0.1.60",
22
+ ]
23
+
24
+ [project.scripts]
25
+ redeploy = "redeploy.cli:cli"
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8.0", "ruff>=0.4",
29
+ "goal>=2.1.0",
30
+ "costs>=0.1.20",
31
+ "pfix>=0.1.60",
32
+ ]
33
+
34
+ [tool.ruff]
35
+ line-length = 100
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["."]
39
+ include = ["redeploy*"]
40
+
41
+ [tool.pfix]
42
+ # Self-healing Python configuration
43
+ model = "openrouter/qwen/qwen3-coder-next"
44
+ auto_apply = true
45
+ auto_install_deps = true
46
+ auto_restart = false
47
+ max_retries = 3
48
+ create_backups = false
49
+ git_auto_commit = false
50
+
51
+ [tool.pfix.runtime_todo]
52
+ enabled = true
53
+ todo_file = "TODO.md"
54
+ min_severity = "low"
55
+ deduplicate = true
56
+
57
+ [tool.costs]
58
+ # AI Cost tracking configuration
59
+ badge = true
60
+ update_readme = true
61
+ readme_path = "README.md"
62
+ default_model = "openrouter/qwen/qwen3-coder-next"
63
+ analysis_mode = "byok"
64
+ full_history = true
65
+ max_commits = 500
66
+
67
+ # Cost thresholds for badge colors (USD)
68
+ badge_color_thresholds = { low = 1.0, medium = 5.0, high = 10.0, critical = 50.0 }
@@ -0,0 +1,2 @@
1
+ """redeploy — Infrastructure migration toolkit: detect → plan → apply."""
2
+ __version__ = "0.1.1"
@@ -0,0 +1,4 @@
1
+ """apply — Execute a MigrationPlan step by step."""
2
+ from .executor import Executor
3
+
4
+ __all__ = ["Executor"]
@@ -0,0 +1,199 @@
1
+ """Executor — runs MigrationPlan steps, handles rollback on failure."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import httpx
10
+ import yaml
11
+ from loguru import logger
12
+
13
+ from ..detect.remote import RemoteProbe
14
+ from ..models import MigrationPlan, MigrationStep, StepAction, StepStatus
15
+
16
+
17
+ class StepError(Exception):
18
+ def __init__(self, step: MigrationStep, msg: str):
19
+ self.step = step
20
+ super().__init__(f"[{step.id}] {msg}")
21
+
22
+
23
+ class Executor:
24
+ """Execute MigrationPlan steps on a remote host."""
25
+
26
+ def __init__(self, plan: MigrationPlan, dry_run: bool = False):
27
+ self.plan = plan
28
+ self.dry_run = dry_run
29
+ self.probe = RemoteProbe(plan.host)
30
+ self._completed: list[MigrationStep] = []
31
+
32
+ def run(self) -> bool:
33
+ """Execute all steps. Returns True if all passed."""
34
+ prefix = "[DRY RUN] " if self.dry_run else ""
35
+ logger.info(f"{prefix}Applying plan: {len(self.plan.steps)} steps "
36
+ f"({self.plan.from_strategy.value} → {self.plan.to_strategy.value})")
37
+
38
+ for step in self.plan.steps:
39
+ try:
40
+ self._execute_step(step)
41
+ self._completed.append(step)
42
+ except StepError as e:
43
+ logger.error(f"Step failed: {e}")
44
+ step.status = StepStatus.FAILED
45
+ step.error = str(e)
46
+ if not self.dry_run:
47
+ self._rollback()
48
+ return False
49
+
50
+ logger.info(f"{'[DRY RUN] ' if self.dry_run else ''}All {len(self.plan.steps)} steps completed")
51
+ return True
52
+
53
+ # ── step dispatcher ───────────────────────────────────────────────────────
54
+
55
+ def _execute_step(self, step: MigrationStep) -> None:
56
+ logger.info(f" {'[DRY]' if self.dry_run else '→'} [{step.id}] {step.description}")
57
+ step.status = StepStatus.RUNNING
58
+
59
+ if self.dry_run:
60
+ step.status = StepStatus.DONE
61
+ step.result = "dry-run"
62
+ return
63
+
64
+ dispatch = {
65
+ StepAction.SYSTEMCTL_STOP: self._run_ssh,
66
+ StepAction.SYSTEMCTL_DISABLE: self._run_ssh,
67
+ StepAction.SYSTEMCTL_START: self._run_ssh,
68
+ StepAction.KUBECTL_DELETE: self._run_ssh,
69
+ StepAction.DOCKER_COMPOSE_UP: self._run_ssh,
70
+ StepAction.DOCKER_COMPOSE_DOWN: self._run_ssh,
71
+ StepAction.DOCKER_BUILD: self._run_ssh,
72
+ StepAction.SSH_CMD: self._run_ssh,
73
+ StepAction.SCP: self._run_scp,
74
+ StepAction.RSYNC: self._run_rsync,
75
+ StepAction.HTTP_CHECK: self._run_http_check,
76
+ StepAction.VERSION_CHECK: self._run_version_check,
77
+ StepAction.WAIT: self._run_wait,
78
+ }
79
+
80
+ handler = dispatch.get(step.action)
81
+ if not handler:
82
+ raise StepError(step, f"No handler for action {step.action}")
83
+ handler(step)
84
+
85
+ # ── handlers ─────────────────────────────────────────────────────────────
86
+
87
+ def _run_ssh(self, step: MigrationStep) -> None:
88
+ cmd = step.command
89
+ if not cmd:
90
+ raise StepError(step, "No command specified")
91
+ r = self.probe.run(cmd, timeout=300)
92
+ step.result = r.out[:500]
93
+ if not r.ok:
94
+ raise StepError(step, f"exit={r.returncode}: {r.stderr[:200]}")
95
+ step.status = StepStatus.DONE
96
+
97
+ def _run_scp(self, step: MigrationStep) -> None:
98
+ if not step.src or not step.dst:
99
+ raise StepError(step, "scp requires src and dst")
100
+ if self.probe.is_local:
101
+ cmd = ["cp", step.src, step.dst]
102
+ else:
103
+ cmd = ["scp", "-o", "StrictHostKeyChecking=no",
104
+ step.src, f"{self.plan.host}:{step.dst}"]
105
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
106
+ if result.returncode != 0:
107
+ raise StepError(step, f"scp failed: {result.stderr[:200]}")
108
+ step.status = StepStatus.DONE
109
+ step.result = "ok"
110
+
111
+ def _run_rsync(self, step: MigrationStep) -> None:
112
+ if not step.src or not step.dst:
113
+ raise StepError(step, "rsync requires src and dst")
114
+ if self.probe.is_local:
115
+ dst = step.dst
116
+ else:
117
+ dst = f"{self.plan.host}:{step.dst}"
118
+ cmd = ["rsync", "-az", "--delete", step.src, dst]
119
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
120
+ if result.returncode != 0:
121
+ raise StepError(step, f"rsync failed: {result.stderr[:200]}")
122
+ step.status = StepStatus.DONE
123
+ step.result = "ok"
124
+
125
+ def _run_http_check(self, step: MigrationStep, retries: int = 5, delay: int = 8) -> None:
126
+ if not step.url:
127
+ raise StepError(step, "http_check requires url")
128
+ last_err = ""
129
+ for attempt in range(retries):
130
+ try:
131
+ r = httpx.get(step.url, timeout=10, verify=False, follow_redirects=True)
132
+ body = r.text
133
+ if step.expect and step.expect not in body:
134
+ last_err = f"expected '{step.expect}' not found in response"
135
+ elif r.status_code >= 400:
136
+ last_err = f"HTTP {r.status_code}"
137
+ else:
138
+ step.status = StepStatus.DONE
139
+ step.result = body[:200]
140
+ return
141
+ except Exception as e:
142
+ last_err = str(e)
143
+ logger.debug(f" retry {attempt + 1}/{retries}: {last_err}")
144
+ time.sleep(delay)
145
+ raise StepError(step, f"HTTP check failed after {retries} retries: {last_err}")
146
+
147
+ def _run_version_check(self, step: MigrationStep) -> None:
148
+ if not step.url or not step.expect:
149
+ raise StepError(step, "version_check requires url and expect")
150
+ try:
151
+ r = httpx.get(step.url, timeout=10, verify=False, follow_redirects=True)
152
+ body = r.text
153
+ if step.expect not in body:
154
+ raise StepError(step, f"version '{step.expect}' not found in response: {body[:100]}")
155
+ step.status = StepStatus.DONE
156
+ step.result = f"version {step.expect} confirmed"
157
+ except StepError:
158
+ raise
159
+ except Exception as e:
160
+ raise StepError(step, str(e))
161
+
162
+ def _run_wait(self, step: MigrationStep) -> None:
163
+ if step.seconds > 0:
164
+ logger.debug(f" waiting {step.seconds}s...")
165
+ time.sleep(step.seconds)
166
+ step.status = StepStatus.DONE
167
+ step.result = f"waited {step.seconds}s"
168
+
169
+ # ── rollback ──────────────────────────────────────────────────────────────
170
+
171
+ def _rollback(self) -> None:
172
+ logger.warning("Rolling back completed steps...")
173
+ for step in reversed(self._completed):
174
+ if step.rollback_command:
175
+ logger.info(f" ↩ rollback [{step.id}]: {step.rollback_command}")
176
+ r = self.probe.run(step.rollback_command, timeout=120)
177
+ if not r.ok:
178
+ logger.warning(f" rollback failed: {r.stderr[:100]}")
179
+
180
+ # ── summary ───────────────────────────────────────────────────────────────
181
+
182
+ def summary(self) -> str:
183
+ total = len(self.plan.steps)
184
+ done = sum(1 for s in self.plan.steps if s.status == StepStatus.DONE)
185
+ failed = sum(1 for s in self.plan.steps if s.status == StepStatus.FAILED)
186
+ icon = "✅" if failed == 0 else "❌"
187
+ return f"{icon} {done}/{total} steps completed" + (f", {failed} failed" if failed else "")
188
+
189
+ @staticmethod
190
+ def from_file(plan_path: Path) -> "Executor":
191
+ with plan_path.open() as f:
192
+ raw = yaml.safe_load(f)
193
+ plan = MigrationPlan(**raw)
194
+ return Executor(plan)
195
+
196
+ def save_results(self, output: Path) -> None:
197
+ data = self.plan.model_dump(mode="json")
198
+ output.write_text(yaml.dump(data, default_flow_style=False, allow_unicode=True))
199
+ logger.info(f"Results saved to {output}")