diff --git a/.gitignore b/.gitignore index 32a1ce8..00676e1 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,10 @@ HAR/*.har # === Tokens / secrets === *.token -secrets/*.token secrets/private_key.asc -secrets/*_key* -secrets/*.pem +secrets/.s3cfg_registry +secrets/pearlharbor_registry.txt +secrets/id_ed25519.txt # === OS === .DS_Store diff --git a/secrets/.gitkeep b/secrets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/secrets/HOW_TO_SSH.md b/secrets/HOW_TO_SSH.md new file mode 100644 index 0000000..9e0bebb --- /dev/null +++ b/secrets/HOW_TO_SSH.md @@ -0,0 +1,37 @@ +# Как выполнять команды на удалённом сервере + +**Сервер**: `naeel@5.172.178.213` +**SSH-ключ**: `/home/naeel/remote_dev/terraform/secrets/id_ed25519.txt` +**Репозиторий на сервере**: `/home/naeel/terra/terraform` + +## Разовая команда + +```bash +ssh -o StrictHostKeyChecking=no -i /home/naeel/remote_dev/terraform/secrets/id_ed25519.txt naeel@5.172.178.213 "КОМАНДА" +``` + +## Git push (с токеном) + +```bash +ssh -o StrictHostKeyChecking=no -i /home/naeel/remote_dev/terraform/secrets/id_ed25519.txt naeel@5.172.178.213 \ + "cd /home/naeel/terra/terraform && git push https://naeel:GITEA_TOKEN@gitea-naeel.giteak8s.services.ngcloud.ru/naeel/terraform.git BRANCH" +``` + +## Git commit + push (полный цикл) + +```bash +ssh -o StrictHostKeyChecking=no -i /home/naeel/remote_dev/terraform/secrets/id_ed25519.txt naeel@5.172.178.213 \ + "cd /home/naeel/terra/terraform && git add -A && git commit -m 'MESSAGE' && git push https://naeel:GITEA_TOKEN@gitea-naeel.giteak8s.services.ngcloud.ru/naeel/terraform.git BRANCH" +``` + +## Интерактивный сеанс (не из агента) + +```bash +ssh -i /home/naeel/remote_dev/terraform/secrets/id_ed25519.txt naeel@5.172.178.213 +``` + +## Примечания + +- sshfs монтирует `/home/naeel/terra` → `/home/naeel/remote_dev` +- Git нужно запускать через SSH на сервере, локальный git в смонтированной папке не работает +- Gitea PAT хранить отдельно (не коммитить) diff --git a/secrets/id_ed25519.pub b/secrets/id_ed25519.pub new file mode 100755 index 0000000..d38dc42 --- /dev/null +++ b/secrets/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPR8S07Mnku1VlVR/lq6hCKPo9fNzJ+7E0DoE7bkvy4p tazet@narod.ru diff --git a/secrets/public_key.asc b/secrets/public_key.asc new file mode 100644 index 0000000..4f0f5fc --- /dev/null +++ b/secrets/public_key.asc @@ -0,0 +1,63 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=q8Tw +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tools/debug_req.go b/tools/debug_req.go new file mode 100644 index 0000000..c1019da --- /dev/null +++ b/tools/debug_req.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// Token from terraform.tfvars (simplified) +const token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJUdzZMa1FRS1ctZHlpb0hoYXlxOHRxa2dhb2RqQjM0bVAtSDBjUkpGWkRJIn0.eyJleHAiOjE3Njk4NDQxMTAsImlhdCI6MTc2OTgzNjkxMCwiYXV0aF90aW1lIjoxNzY5ODM2ODg5LCJqdGkiOiIzMGM2MjZlMC1kMDY2LTQzNTItODFiMC0yZGM4Yjk0ODM5NzIiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLm51YmVzLnJ1L3JlYWxtcy9jbG91ZCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJhNTVhNjFlZC1iMTU5LTQ3YjYtYmIxNi00N2Q4ZjIyZDQ4MGIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhY2NvdW50LWNvbnNvbGUiLCJub25jZSI6IjM2ODc0MDljLWU1MGQtNGVkMi1iY2JkLTU1ODA2MmNlY2ZkNCIsInNlc3Npb25fc3RhdGUiOiI1YTYxMTdkMS03ZTI2LTQ3YTQtOWUxYS0xZTFlZTViYTlmNzQiLCJhY3IiOiIwIiwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiI1YTYxMTdkMS03ZTI2LTQ3YTQtOWUxYS0xZTFlZTViYTlmNzQiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6ItCi0LDQt9C10YLQtNC40L3QvtCyINCd0LDQuNC70YwiLCJDbGllbnRJRCI6IldaMDEzMjUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0YXpldEBuYXJvZC5ydSIsImxvY2FsZSI6InJ1IiwiZ2l2ZW5fbmFtZSI6ItCi0LDQt9C10YLQtNC40L3QvtCyIiwiZmFtaWx5X25hbWUiOiLQndCw0LjQu9GMIiwiZW1haWwiOiJ0YXpldEBuYXJvZC5ydSJ9.gqIGJ5aH1nqBqpnvVOtZWXMt3pZp5m264xQ3ar2D1rUOh0NHSFtoIKSjJkgSlmFeFMjd-C261MQDg78YwGzYn2RH8hLJq-ht0l07-ZGr9bqYNyjcNTyeQ_bjqA5sGBKzTd1XfA4jsuMqZ6rIZAsy_NnVunHodivHSSKv7LdArZeyTeOIFVojattX96CDiBLnM45utOtS0NM3Ae8rowKJWj9l46N4c8n8u-zJ6rx5eP50LOYjz1sod7lwcgatTATDHDwH0moYabzPdeOvK-xpbSmibMhKUpaHLNnVyL6qTqn0zh_S81mXeqZmdXozlhrjnF3wCUdz0rXDJqmBnq50fA" + +func main() { + url := "https://deck-api.ngcloud.ru/api/v1/index.cfm/instances" + payload := map[string]interface{}{ + "serviceId": 1, + "displayName": "DebugReq-Test", + "descr": "Created via debug tool", + } + b, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(b)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Connection", "close") // Try to force close + + client := &http.Client{Timeout: 10 * time.Second} + fmt.Println("Sending request...") + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + defer resp.Body.Close() + + fmt.Printf("Status: %s\n", resp.Status) + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Body: %s\n", string(body)) +} diff --git a/tools/docs_publish/README.md b/tools/docs_publish/README.md new file mode 100644 index 0000000..86bf764 --- /dev/null +++ b/tools/docs_publish/README.md @@ -0,0 +1,162 @@ +# Публикация документации (Nubes Terraform Provider) + +## Назначение +Эта инструкция описывает полный цикл: генерация страниц ресурсов, сборка сайта и загрузка на https://terra.k8c.ru. + +## Требования +- Docker +- `mc` (S3 client) +- Доступ к S3 (переменные `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`) + +## 1) Генерация страниц ресурсов +Источники параметров: `universal_rebuild/resources_yaml/*.yaml`. + +Генератор страниц (создаёт/обновляет файлы в `docs/30_registry/resources/` и индекс): + +```bash +/home/naeel/terra/.venv/bin/python - <<'PY' +import glob +import os +import re +import yaml + +resources_yaml_dir = "/home/naeel/terra/universal_rebuild/resources_yaml" +resources_gen_dir = "/home/naeel/terra/universal_rebuild/internal/resources_gen" +resources_docs_dir = "/home/naeel/terra/docs/30_registry/resources" +index_path = os.path.join(resources_docs_dir, "index.md") + +os.makedirs(resources_docs_dir, exist_ok=True) + +def parse_computed_fields(go_path: str): + try: + with open(go_path, "r", encoding="utf-8") as f: + lines = f.readlines() + except FileNotFoundError: + return [] + + computed = [] + i = 0 + while i < len(lines): + line = lines[i] + m = re.search(r'"([a-zA-Z0-9_]+)"\s*:\s*schema\.[A-Za-z]+Attribute\{', line) + if not m: + i += 1 + continue + name = m.group(1) + block_lines = [line] + i += 1 + while i < len(lines) and "}," not in lines[i]: + block_lines.append(lines[i]) + i += 1 + if i < len(lines): + block_lines.append(lines[i]) + block_text = "".join(block_lines) + if "Computed: true" in block_text: + computed.append(name) + i += 1 + return sorted(set(computed)) + +computed_by_name = {} +for go_path in glob.glob(os.path.join(resources_gen_dir, "*_resource.go")): + name = None + with open(go_path, "r", encoding="utf-8") as f: + for line in f: + if line.startswith("// Service:"): + name = line.split(":", 1)[1].strip() + break + if not name: + continue + computed_by_name[name] = parse_computed_fields(go_path) + +files = sorted(glob.glob(os.path.join(resources_yaml_dir, "service_*.yaml"))) +resource_names = [] + +for path in files: + with open(path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + if not data: + continue + name = data.get("name", "unknown") + service_id = data.get("service_id", "unknown") + resource_names.append(name) + + create = data.get("create", {}).get("params", []) or [] + modify = data.get("modify", {}).get("params", []) or [] + computed = computed_by_name.get(name, []) + + doc_path = os.path.join(resources_docs_dir, f"{name}.md") + lines = [] + lines.append(f"# Resource nubes_{name}\n\n") + lines.append(f"Service ID: `{service_id}`\n\n") + + lines.append("## Output поля\n\n") + if computed: + for c in computed: + lines.append(f"- `{c}`\n") + else: + lines.append("Нет.\n") + lines.append("\n") + + lines.append("## Create параметры\n\n") + if create: + lines.append("| Code | Type | Required | Default | ID |\n") + lines.append("|---|---|---|---|---|\n") + for p in create: + code = p.get("code", "") + ptype = p.get("type", "") + required = str(p.get("required", False)).lower() + default = p.get("default", "") + pid = p.get("id", "") + lines.append(f"| `{code}` | `{ptype}` | `{required}` | `{default}` | `{pid}` |\n") + else: + lines.append("Нет.\n") + + lines.append("\n## Modify параметры\n\n") + if modify: + lines.append("| Code | Type | Required | Default | ID |\n") + lines.append("|---|---|---|---|---|\n") + for p in modify: + code = p.get("code", "") + ptype = p.get("type", "") + required = str(p.get("required", False)).lower() + default = p.get("default", "") + pid = p.get("id", "") + lines.append(f"| `{code}` | `{ptype}` | `{required}` | `{default}` | `{pid}` |\n") + else: + lines.append("Нет.\n") + + with open(doc_path, "w", encoding="utf-8") as f: + f.write("".join(lines)) + +resource_names_sorted = sorted(set(resource_names)) +index_lines = ["# Resources\n\n", "Список всех ресурсов провайдера.\n\n"] +for name in resource_names_sorted: + index_lines.append(f"- [{name}](./{name}.md)\n") + +with open(index_path, "w", encoding="utf-8") as f: + f.write("".join(index_lines)) + +print(f"Generated {len(resource_names_sorted)} resource pages.") +print(f"Wrote {index_path}") +PY +``` + +## 2) Навигация mkdocs +Проверьте, что `mkdocs.yml` содержит: +- `Resources` (index) +- `Guides` (getting-started, terraform-basics) + +## 3) Сборка сайта +```bash +docker run --rm -v ${PWD}:/docs squidfunk/mkdocs-material build +``` + +## 4) Публикация +```bash +set -a && . /home/naeel/terra/.env.s3 && set +a +/home/naeel/terra/scripts/publish-docs.sh site terra.k8c.ru nubes nubes 2.0.1 +``` + +## Результат +Документация должна открываться по адресу: +`https://terra.k8c.ru/docs/nubes/nubes/2.0.1/` diff --git a/tools/har/analyze_f12_har.py b/tools/har/analyze_f12_har.py new file mode 100644 index 0000000..44c80cf --- /dev/null +++ b/tools/har/analyze_f12_har.py @@ -0,0 +1,62 @@ +import json +import sys + +def analyze_har(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + har_data = json.load(f) + + target_uid = "79ac8a4a-353b-4f3f-8ab1-fa937d2d23f8" + + entries = har_data['log']['entries'] + + found_responses = [] + + for entry in entries: + request = entry['request'] + response = entry['response'] + + # Check if the URL contains the UID or the request/response body contains it + url = request.get('url', '') + + resp_text = "" + if 'content' in response and 'text' in response['content']: + resp_text = response['content']['text'] + + if target_uid in url or target_uid in resp_text: + found_responses.append({ + 'url': url, + 'method': request['method'], + 'status': response['status'], + 'time': entry['startedDateTime'], + 'response': resp_text + }) + + # Sort by time + found_responses.sort(key=lambda x: x['time']) + + for i, item in enumerate(found_responses): + print(f"--- Entry {i+1} ---") + print(f"Time: {item['time']}") + print(f"Method: {item['method']}") + print(f"URL: {item['url']}") + print(f"Status: {item['status']}") + + if item['response']: + try: + data = json.loads(item['response']) + # Only print interesting parts to avoid flooding + if 'instanceOperation' in data: + op = data['instanceOperation'] + print(f"Operation: {op.get('operation')} Status: {op.get('isSuccessful')} Progress: {op.get('isInProgress')}") + if 'stages' in op: + for stage in op['stages']: + print(f" Stage: {stage.get('stage')} Successs: {stage.get('isSuccessful')} Msg: {stage.get('stageMsg')}") + elif 'instance' in data: + inst = data['instance'] + print(f"Instance Status: {inst.get('explainedStatus')}") + except: + print("Response is not JSON or parsing failed") + print("\n") + +if __name__ == "__main__": + analyze_har("/home/naeel/terra/har/f12vmbad.har") diff --git a/tools/har/analyze_faststart.py b/tools/har/analyze_faststart.py new file mode 100644 index 0000000..2f7ca78 --- /dev/null +++ b/tools/har/analyze_faststart.py @@ -0,0 +1,98 @@ +import json +import os +from datetime import datetime + +har_path = '/home/naeel/terra/har/faststart.har' + +def analyze_har(): + with open(har_path, 'r', encoding='utf-8') as f: + har_data = json.load(f) + + entries = har_data['log']['entries'] + + instance_id = None + operation_id = None + start_time = None + end_time = None + error_message = None + + # Sort entries by start time + entries.sort(key=lambda x: x['startedDateTime']) + + for entry in entries: + req = entry['request'] + res = entry['response'] + url = req['url'] + + # Look for instance UID in body + if req.get('postData', {}).get('text'): + if 'instanceUid' in req['postData']['text']: + try: + body = json.loads(req['postData']['text']) + instance_id = body.get('instanceUid') + except: pass + + # Look for operation creation or status + if '/instanceOperations' in url: + if req['method'] == 'POST' and res['status'] == 201: + try: + body = json.loads(res['content'].get('text', '{}')) + operation_id = body.get('uid') + start_time = entry['startedDateTime'] + print(f"Operation started: {operation_id} at {start_time}") + except: pass + + # Match the UUID from the URL if not found in POST + if not operation_id: + parts = url.split('/') + for part in parts: + if len(part) == 36 and '-' in part: # UUID shape + operation_id = part + break + + # Track operation progress/status + if operation_id and f'/instanceOperations/{operation_id}' in url and req['method'] == 'GET': + try: + text = res['content'].get('text', '{}') + if not text: continue + body = json.loads(text) + if isinstance(body, list): + body = body[0] if body else {} + status = body.get('status') + print(f"Polling {operation_id}: status={status} at {entry['startedDateTime']}") + # print(f"DEBUG: {text[:200]}") # Print first 200 chars + if status in ['failed', 'completed', 'success', 'error', 'failed-manual-fix']: + if not end_time or entry['startedDateTime'] > end_time: + end_time = entry['startedDateTime'] + error_message = body.get('error') or body.get('message') or body.get('statusMessage') + if not error_message and body.get('steps'): + # Check steps for errors + for step in body.get('steps'): + if step.get('status') == 'failed': + error_message = step.get('error') or step.get('statusMessage') + print(f"Operation reached terminal status {status} at {end_time}") + print(f"Full response: {text}") + except Exception as e: + # print(f"Error parsing: {e}") + pass + + # Check for Sun/Date anomalies in headers + for header in res['headers']: + if header['name'].lower() == 'date': + if 'Sun' in header['value']: + print(f"Strange Date header found: {header['value']} in {url}") + + print(f"\nSummary:") + print(f"Instance ID: {instance_id}") + print(f"Operation ID: {operation_id}") + if start_time and end_time: + t1 = datetime.fromisoformat(start_time.replace('Z', '+00:00')) + t2 = datetime.fromisoformat(end_time.replace('Z', '+00:00')) + duration = (t2 - t1).total_seconds() + print(f"Duration: {duration} seconds") + else: + print("Could not determine duration") + print(f"Error: {error_message}") + +if __name__ == "__main__": + analyze_har() diff --git a/tools/har/analyze_vmbad_params.py b/tools/har/analyze_vmbad_params.py new file mode 100644 index 0000000..cbeaf56 --- /dev/null +++ b/tools/har/analyze_vmbad_params.py @@ -0,0 +1,47 @@ +import json +import os + +def extract_params_from_har(har_path): + print(f"\n--- Analyzing {os.path.basename(har_path)} ---") + if not os.path.exists(har_path): + print(f"File not found: {har_path}") + return + + with open(har_path, 'r', encoding='utf-8') as f: + har_data = json.load(f) + + entries = har_data['log']['entries'] + + # Track operations to group parameters + operations = {} # UID -> list of params + + for entry in entries: + req = entry['request'] + # Looking for POST /api/v1/index.cfm/instanceOperationCfsParams + if '/instanceOperationCfsParams' in req['url'] and req['method'] == 'POST': + post_data = req.get('postData', {}) + text = post_data.get('text') + if text: + try: + p = json.loads(text) + op_uid = p.get('instanceOperationUid') + if op_uid not in operations: + operations[op_uid] = [] + operations[op_uid].append(p) + except: + pass + + for op_uid, params in operations.items(): + print(f"\nOperation Uid: {op_uid}") + for p in params: + param_id = p.get('svcOperationCfsParamId') + param_val = p.get('paramValue') + print(f" ID: {param_id:4} | Value: {repr(param_val)}") + +har_files = [ + '/home/naeel/terra/har/f12vmbad.har', + '/home/naeel/terra/har/f12vmbad1.har' +] + +for h in har_files: + extract_params_from_har(h) diff --git a/tools/har/check_op.py b/tools/har/check_op.py new file mode 100644 index 0000000..f1141a4 --- /dev/null +++ b/tools/har/check_op.py @@ -0,0 +1,22 @@ +import json + +har_path = '/home/naeel/terra/har/faststart.har' +op_id = 'c2e44fae-2a05-4955-867c-763da66b578e' + +with open(har_path, 'r', encoding='utf-8') as f: + har_data = json.load(f) + +entries = har_data['log']['entries'] + +for entry in entries: + url = entry['request']['url'] + if op_id in url and entry['request']['method'] == 'GET': + res = entry['response'] + text = res['content'].get('text') + if text: + try: + data = json.loads(text) + print(f"Time: {entry['startedDateTime']}") + print(json.dumps(data, indent=2)) + except Exception as e: + print(f"Error: {e}") diff --git a/tools/har/check_status.py b/tools/har/check_status.py new file mode 100644 index 0000000..1a72984 --- /dev/null +++ b/tools/har/check_status.py @@ -0,0 +1,33 @@ +import json +import os + +def check_creation_status(har_path): + print(f"--- Checking status in {os.path.basename(har_path)} ---") + with open(har_path, 'r', encoding='utf-8') as f: + har_data = json.load(f) + + entries = har_data['log']['entries'] + + for entry in entries: + req = entry['request'] + resp = entry['response'] + + # Check instanceOperations POST + if '/instanceOperations' in req['url'] and req['method'] == 'POST' and not '/instanceOperationCfsParams' in req['url']: + print(f"Found Operation POST: {req['url']}") + post_data = req.get('postData', {}) + print(f"Request body: {post_data.get('text')}") + print(f"Response status: {resp['status']}") + resp_content = resp.get('content', {}) + print(f"Response body: {resp_content.get('text')}") + print("-" * 20) + +har_files = [ + '/home/naeel/terra/har/f12vmbad.har', + '/home/naeel/terra/har/f12vmbad1.har', + '/home/naeel/terra/har/faststart.har' +] + +for h in har_files: + if os.path.exists(h): + check_creation_status(h) diff --git a/tools/har/compare_har.py b/tools/har/compare_har.py new file mode 100644 index 0000000..394de49 --- /dev/null +++ b/tools/har/compare_har.py @@ -0,0 +1,94 @@ +import json +import re + +def get_ids_and_failures(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + har_data = json.load(f) + + entries = har_data['log']['entries'] + + instance_ids = set() + operation_ids = set() + dates = set() + + # Try to find UUIDs that look like instance or operation IDs + # Usually in URLs or body + for entry in entries: + req = entry['request'] + resp = entry['response'] + + # Check Date header + for header in resp.get('headers', []): + if header['name'].lower() == 'date': + dates.add(header['value']) + + url = req.get('url', '') + # Pattern for UUID + uuids = re.findall(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', url) + for u in uuids: + instance_ids.add(u) + + resp_text = "" + if 'content' in resp and 'text' in resp['content']: + resp_text = resp['content']['text'] + # Look for instanceOperation or similar + if 'instanceOperation' in resp_text: + op_uuids = re.findall(r'"id":"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"', resp_text) + for u in op_uuids: + operation_ids.add(u) + + # Look for instance id + inst_uuids = re.findall(r'"instanceId":"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"', resp_text) + for u in inst_uuids: + instance_ids.add(u) + + print(f"File: {file_path}") + print(f"Possible Instance IDs: {instance_ids}") + print(f"Possible Operation IDs: {operation_ids}") + + # Analyze flow + print("\nFlow analysis (Status 400+ or instanceOperation):") + for entry in entries: + req = entry['request'] + resp = entry['response'] + status = resp['status'] + url = req['url'] + time = entry['startedDateTime'] + + if status >= 400: + print(f"[{time}] ERROR {status} {req['method']} {url}") + if 'content' in resp and 'text' in resp['content']: + print(f" Response: {resp['content']['text'][:500]}") + + if 'instanceOperation' in url or ('content' in resp and 'text' in resp['content'] and 'instanceOperation' in resp['content']['text']): + print(f"[{time}] OP_POLL {status} {url}") + if 'content' in resp and 'text' in resp['content']: + try: + data = json.loads(resp['content']['text']) + if isinstance(data, dict): + op = data.get('instanceOperation', data) + if isinstance(op, dict): + success = op.get('isSuccessful') + if success is False: + # Print summary of stages + stages = op.get('stages', []) + print(f" FAILED OP STAGES:") + for s in stages: + print(f" - {s.get('displayName')} Status: {s.get('isSuccessful')} Msg: {s.get('message')}") + print(f" FULL ERROR LOG: {op.get('errorLog')}") + else: + print(f" Op: {op.get('operation')} Success: {success} Progress: {op.get('isInProgress')} Msg: {op.get('message')}") + except: + pass + + print("\n--- Date headers summary ---") + sun_mon = [d for d in dates if "Sun" in d or "Mon" in d] + if sun_mon: + print(f"Found Sun/Mon dates: {sun_mon[:5]}") + else: + print("No Sun/Mon dates found in headers.") + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + get_ids_and_failures(sys.argv[1]) diff --git a/tools/har/extract_params.py b/tools/har/extract_params.py new file mode 100644 index 0000000..89d8974 --- /dev/null +++ b/tools/har/extract_params.py @@ -0,0 +1,42 @@ +import json +import os + +def extract_params_from_har(har_path): + print(f"--- Analyzing {os.path.basename(har_path)} ---") + with open(har_path, 'r', encoding='utf-8') as f: + har_data = json.load(f) + + entries = har_data['log']['entries'] + + # Store parameters group by operation (if possible) + # Looking for POST /api/v1/index.cfm/instanceOperationCfsParams + + results = [] + for entry in entries: + req = entry['request'] + if '/instanceOperationCfsParams' in req['url'] and req['method'] == 'POST': + post_data = req.get('postData', {}) + text = post_data.get('text') + if text: + try: + params = json.loads(text) + results.append(params) + except: + pass + + for p in results: + # Expected keys: instanceOperationUid, svcOperationCfsParamId, paramValue + op_uid = p.get('instanceOperationUid') + param_id = p.get('svcOperationCfsParamId') + param_val = p.get('paramValue') + print(f"Op: {op_uid} | ID: {param_id} | Value: {param_val} (Type: {type(param_val).__name__})") + +har_files = [ + '/home/naeel/terra/har/vm.har' +] + +for h in har_files: + if os.path.exists(h): + extract_params_from_har(h) + else: + print(f"File not found: {h}") diff --git a/tools/har/find_pid_mapping.py b/tools/har/find_pid_mapping.py new file mode 100644 index 0000000..de173b2 --- /dev/null +++ b/tools/har/find_pid_mapping.py @@ -0,0 +1,51 @@ +import json +import os +import re + +def find_param_defs(har_path): + param_map = {} + with open(har_path, 'r', encoding='utf-8') as f: + har_data = json.load(f) + + for entry in har_data['log']['entries']: + resp = entry['response'] + if 'content' in resp and 'text' in resp['content']: + text = resp['content']['text'] + # Search for pattern "svcOperationCfsParamId":X,"svcOperationCfsParam":"NAME" + # Or "svcOperationCfsParamId":X ... "svcOperationCfsParam":"NAME" + # Matches in JSON structure + try: + data = json.loads(text) + def walk(obj): + if isinstance(obj, dict): + if 'svcOperationCfsParamId' in obj and 'svcOperationCfsParam' in obj: + param_map[obj['svcOperationCfsParamId']] = { + 'name': obj['svcOperationCfsParam'], + 'label': obj.get('label'), + 'dataType': obj.get('dataType'), + 'isRequired': obj.get('isRequired') + } + for v in obj.values(): + walk(v) + elif isinstance(obj, list): + for item in obj: + walk(item) + walk(data) + except: + pass + return param_map + +har_files = [ + '/home/naeel/terra/har/f12vmbad.har', + '/home/naeel/terra/har/f12vmbad1.har', + '/home/naeel/terra/har/faststart.har' +] + +all_params = {} +for h in har_files: + if os.path.exists(h): + all_params.update(find_param_defs(h)) + +# Print mapping +for pid in sorted(all_params.keys()): + print(f"{pid}: {all_params[pid]}") diff --git a/tools/mariadb_check_container/Dockerfile b/tools/mariadb_check_container/Dockerfile new file mode 100644 index 0000000..2d70862 --- /dev/null +++ b/tools/mariadb_check_container/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py ./ + +EXPOSE 8080 + +CMD ["python", "app.py"] diff --git a/tools/mariadb_check_container/README.md b/tools/mariadb_check_container/README.md new file mode 100644 index 0000000..826b106 --- /dev/null +++ b/tools/mariadb_check_container/README.md @@ -0,0 +1,24 @@ +# MariaDB check container + +Small HTTP service that performs `SELECT 1` against MariaDB and returns JSON. + +## Environment variables + +- `DB_HOST` +- `DB_PORT` (default: 3306) +- `DB_USER` +- `DB_PASSWORD` +- `DB_NAME` (default: postgres) +- `PORT` (default: 8080) + +## Build and push (run locally) + +```bash +# Example: set your registry path +REGISTRY_PATH=pearlharbor.registryk8s.services.ngcloud.ru/demo/mariadb-check:0.1 + +cd tools/mariadb_check_container + +docker build -t "$REGISTRY_PATH" . +docker push "$REGISTRY_PATH" +``` diff --git a/tools/mariadb_check_container/app.py b/tools/mariadb_check_container/app.py new file mode 100644 index 0000000..3349aa7 --- /dev/null +++ b/tools/mariadb_check_container/app.py @@ -0,0 +1,71 @@ +import json +import os +import socket +from http.server import BaseHTTPRequestHandler, HTTPServer + +import pymysql + + +def get_env(name, default=None): + value = os.environ.get(name, default) + return value if value is not None else default + + +def db_check(): + host = get_env("DB_HOST") + port = int(get_env("DB_PORT", "3306")) + user = get_env("DB_USER") + password = get_env("DB_PASSWORD") + dbname = get_env("DB_NAME", "postgres") + + if not host or not user or not password: + return False, "missing DB_HOST/DB_USER/DB_PASSWORD" + + conn = pymysql.connect( + host=host, + port=port, + user=user, + password=password, + database=dbname, + connect_timeout=5, + read_timeout=5, + write_timeout=5, + ) + try: + with conn.cursor() as cur: + cur.execute("SELECT 1") + cur.fetchone() + return True, "ok" + finally: + conn.close() + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + ok, msg = False, "" + status = 500 + try: + ok, msg = db_check() + status = 200 if ok else 500 + except Exception as exc: # pylint: disable=broad-except + msg = str(exc) + + body = { + "status": "ok" if ok else "error", + "message": msg, + "hostname": socket.gethostname(), + } + data = json.dumps(body).encode("utf-8") + + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + +if __name__ == "__main__": + port = int(get_env("PORT", "8080")) + server = HTTPServer(("0.0.0.0", port), Handler) + print(f"listening on :{port}") + server.serve_forever() diff --git a/tools/mariadb_check_container/requirements.txt b/tools/mariadb_check_container/requirements.txt new file mode 100644 index 0000000..df136f1 --- /dev/null +++ b/tools/mariadb_check_container/requirements.txt @@ -0,0 +1 @@ +pymysql==1.1.1 diff --git a/tools/upload_provider_s3.py b/tools/upload_provider_s3.py new file mode 100644 index 0000000..405a758 --- /dev/null +++ b/tools/upload_provider_s3.py @@ -0,0 +1,37 @@ +import configparser +from pathlib import Path + +import boto3 +from botocore.config import Config + +cred_path = Path('/home/naeel/terra/.tmp_aws_credentials') +config = configparser.RawConfigParser() +config.read(cred_path) +key = config.get('registry', 'aws_access_key_id') +secret = config.get('registry', 'aws_secret_access_key') + +cfg = Config(signature_version='s3v4', s3={'addressing_style': 'path', 'payload_signing_enabled': False}) + +s3 = boto3.client( + 's3', + region_name='us-east-1', + endpoint_url='https://s3.msk-1.ngcloud.ru', + aws_access_key_id=key, + aws_secret_access_key=secret, + config=cfg, +) + +bucket = 'terraform-registry' +prefix = 'terrareg.kube5s.ru/nubes/nubes/2.0.0/' +files = [ + 'terraform-provider-nubes_2.0.0_linux_amd64.zip', + 'terraform-provider-nubes_2.0.0_SHA256SUMS', + 'terraform-provider-nubes_2.0.0_SHA256SUMS.sig', +] +base = Path('/home/naeel/terra/nubes_provider_gen') +for name in files: + path = base / name + key_name = prefix + name + with open(path, 'rb') as f: + s3.put_object(Bucket=bucket, Key=key_name, Body=f) + print('uploaded', key_name)