add: tools, secrets (safe files), updated gitignore
This commit is contained in:
+3
-3
@@ -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
|
||||
|
||||
@@ -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 хранить отдельно (не коммитить)
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPR8S07Mnku1VlVR/lq6hCKPo9fNzJ+7E0DoE7bkvy4p tazet@narod.ru
|
||||
@@ -0,0 +1,63 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=q8Tw
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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/`
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
@@ -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}")
|
||||
@@ -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]}")
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
```
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
pymysql==1.1.1
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user