fase 3 (operacion): backups automaticos por instancia con descubrimiento dinamico, verificacion de integridad, retencion GFS 14/8/6, prueba de restauracion semanal, timers systemd y logrotate; panel CLI multi-instancia (admin_instancias.py) + doc de operacion y restauracion
This commit is contained in:
615
infra/ops/admin_instancias.py
Executable file
615
infra/ops/admin_instancias.py
Executable file
@@ -0,0 +1,615 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Panel / CLI de administración multi-instancia de SKEEN.
|
||||
|
||||
Descubre dinámicamente las instancias a partir de las unidades systemd
|
||||
`odoo-*.service` (igual que `backup_instancias.sh` y el provisioning): de cada
|
||||
unidad lee su `-c <conf>` y de ahí la base de datos, el puerto HTTP, el log y el
|
||||
data_dir. No hay ninguna lista de instancias hardcodeada.
|
||||
|
||||
Por defecto todas las consultas son de SOLO LECTURA. Las acciones que cambian
|
||||
estado (`backup`, `reiniciar`) exigen `--yes` y avisan antes de hacer nada.
|
||||
|
||||
Uso:
|
||||
./admin_instancias.py [--json] listar
|
||||
./admin_instancias.py estado <slug>
|
||||
./admin_instancias.py version
|
||||
./admin_instancias.py uso
|
||||
./admin_instancias.py instalacion <slug>
|
||||
./admin_instancias.py logs <slug> [-n N]
|
||||
./admin_instancias.py credenciales <slug> [--mostrar]
|
||||
./admin_instancias.py backup <slug> [--verificar] --yes
|
||||
./admin_instancias.py reiniciar <slug> --yes
|
||||
|
||||
Sin dependencias externas (solo stdlib). Pensado para ejecutarse como root.
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
OPS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ENV_FILE = os.path.join(OPS_DIR, 'backup.env')
|
||||
BACKUP_SCRIPT = os.path.join(OPS_DIR, 'backup_instancias.sh')
|
||||
|
||||
DEFAULT_ENV = {
|
||||
'BACKUP_ROOT': '/root/backups',
|
||||
'LOCK_FILE': '/var/lock/skeen-backup.lock',
|
||||
'LOG_FILE': '/var/log/skeen-backup.log',
|
||||
'RETAIN_DAILY': '14',
|
||||
'RETAIN_WEEKLY': '8',
|
||||
'RETAIN_MONTHLY': '6',
|
||||
}
|
||||
|
||||
KEY_TABLES = ('res_partner', 'skeen_cita', 'skeen_venta')
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- utilidades
|
||||
def load_env(path=ENV_FILE):
|
||||
env = dict(DEFAULT_ENV)
|
||||
if os.path.isfile(path):
|
||||
with open(path, encoding='utf-8') as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or '=' not in line:
|
||||
continue
|
||||
k, v = line.split('=', 1)
|
||||
env[k.strip()] = v.strip()
|
||||
return env
|
||||
|
||||
|
||||
ENV = load_env()
|
||||
|
||||
|
||||
def run(cmd, timeout=30):
|
||||
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
|
||||
def systemctl(*args):
|
||||
return run(['systemctl', *args])
|
||||
|
||||
|
||||
def psql(db, sql):
|
||||
"""Consulta por peer como superusuario local; '' si no se puede."""
|
||||
if not db:
|
||||
return ''
|
||||
cmd = 'psql -d %s -Atc %s' % (shlex.quote(db), shlex.quote(sql))
|
||||
p = run(['su', 'postgres', '-c', cmd])
|
||||
return p.stdout.strip() if p.returncode == 0 else ''
|
||||
|
||||
|
||||
def du_bytes(path):
|
||||
if not path or not os.path.exists(path):
|
||||
return 0
|
||||
p = run(['du', '-sb', path])
|
||||
try:
|
||||
return int(p.stdout.split()[0])
|
||||
except (ValueError, IndexError):
|
||||
return 0
|
||||
|
||||
|
||||
def human(n):
|
||||
try:
|
||||
n = float(n)
|
||||
except (TypeError, ValueError):
|
||||
return '-'
|
||||
for unit in ('B', 'K', 'M', 'G', 'T'):
|
||||
if n < 1024 or unit == 'T':
|
||||
return ('%.0f%s' % (n, unit)) if unit == 'B' else ('%.1f%s' % (n, unit))
|
||||
n /= 1024
|
||||
return '-'
|
||||
|
||||
|
||||
def listening_ports():
|
||||
p = run(['ss', '-ltnH'])
|
||||
return set(int(m) for m in re.findall(r':(\d+)\b', p.stdout))
|
||||
|
||||
|
||||
def http_code(port, path='/web/login', timeout=5):
|
||||
if not port:
|
||||
return None
|
||||
try:
|
||||
req = urllib.request.Request('http://127.0.0.1:%d%s' % (int(port), path))
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.getcode()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ------------------------------------------------------- descubrir instancias
|
||||
def list_unit_names():
|
||||
p = systemctl('list-unit-files', '--type=service', '--no-legend', '--no-pager')
|
||||
names = []
|
||||
for line in p.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if parts and re.match(r'^odoo-.*\.service$', parts[0]):
|
||||
names.append(parts[0])
|
||||
return sorted(names)
|
||||
|
||||
|
||||
def conf_get(path, key):
|
||||
if not os.path.isfile(path):
|
||||
return ''
|
||||
try:
|
||||
with open(path, encoding='utf-8', errors='replace') as fh:
|
||||
inside = False
|
||||
for raw in fh:
|
||||
line = raw.strip()
|
||||
if line.startswith('['):
|
||||
inside = line == '[options]'
|
||||
continue
|
||||
if not inside or not line or line.startswith(('#', ';')) or '=' not in line:
|
||||
continue
|
||||
k, v = line.split('=', 1)
|
||||
if k.strip() == key:
|
||||
return v.strip()
|
||||
except OSError:
|
||||
return ''
|
||||
return ''
|
||||
|
||||
|
||||
def load_instance(unit):
|
||||
frag = systemctl('show', unit, '-p', 'FragmentPath', '--value').stdout.strip()
|
||||
if not frag or not os.path.isfile(frag):
|
||||
return None
|
||||
cat = systemctl('cat', unit).stdout
|
||||
exec_line = ''
|
||||
for line in cat.splitlines():
|
||||
if line.strip().startswith('ExecStart='):
|
||||
exec_line = line.strip()
|
||||
m = re.search(r'\s-c\s+(\S+)', exec_line)
|
||||
if not m:
|
||||
return None
|
||||
conf = m.group(1)
|
||||
if not conf.startswith('/'):
|
||||
conf = os.path.join('/root', conf)
|
||||
db = conf_get(conf, 'db_name')
|
||||
if not db:
|
||||
return None
|
||||
port = conf_get(conf, 'http_port')
|
||||
log = ''
|
||||
m2 = re.search(r'--logfile=(\S+)', exec_line)
|
||||
if m2:
|
||||
log = m2.group(1)
|
||||
log = log or conf_get(conf, 'logfile') or ('/tmp/odoo-%s.log' % unit[len('odoo-'):-len('.service')])
|
||||
slug = unit[len('odoo-'):-len('.service')]
|
||||
return {
|
||||
'slug': slug,
|
||||
'unit': unit,
|
||||
'unit_path': frag,
|
||||
'conf': conf,
|
||||
'db': db,
|
||||
'port': int(port) if port.isdigit() else None,
|
||||
'logfile': log,
|
||||
'data_dir': conf_get(conf, 'data_dir'),
|
||||
}
|
||||
|
||||
|
||||
def discover():
|
||||
out = []
|
||||
for unit in list_unit_names():
|
||||
inst = load_instance(unit)
|
||||
if inst:
|
||||
out.append(inst)
|
||||
return out
|
||||
|
||||
|
||||
def find_vhost(slug, port):
|
||||
for p in ('/etc/nginx/sites-enabled/%s' % slug, '/etc/nginx/sites-available/%s' % slug):
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
if port:
|
||||
for base in ('/etc/nginx/sites-enabled', '/etc/nginx/sites-available'):
|
||||
for f in glob.glob(os.path.join(base, '*')):
|
||||
try:
|
||||
with open(f, encoding='utf-8', errors='replace') as fh:
|
||||
if '127.0.0.1:%d' % port in fh.read():
|
||||
return f
|
||||
except OSError:
|
||||
continue
|
||||
return ''
|
||||
|
||||
|
||||
# ----------------------------------------------------------- datos de instancia
|
||||
def backup_state(slug):
|
||||
d = os.path.join(ENV['BACKUP_ROOT'], slug)
|
||||
dumps = sorted(glob.glob(os.path.join(d, '*.dump')))
|
||||
info = {'dir': d, 'count': len(dumps), 'last': None, 'last_size': 0,
|
||||
'last_mtime': None, 'dir_bytes': 0, 'latest_valid': False}
|
||||
if dumps:
|
||||
last = max(dumps, key=os.path.getmtime)
|
||||
info['last'] = os.path.basename(last)
|
||||
info['last_size'] = os.path.getsize(last)
|
||||
info['last_mtime'] = datetime.fromtimestamp(os.path.getmtime(last)).isoformat(timespec='seconds')
|
||||
info['dir_bytes'] = du_bytes(d) if os.path.isdir(d) else 0
|
||||
return info
|
||||
|
||||
|
||||
def module_versions(db):
|
||||
rows = []
|
||||
out = psql(db, "select name||'|'||coalesce(latest_version,'?') from ir_module_module "
|
||||
"where name like 'skeen_%' and state='installed' order by name")
|
||||
for line in out.splitlines():
|
||||
if '|' in line:
|
||||
n, v = line.split('|', 1)
|
||||
rows.append((n, v))
|
||||
return rows
|
||||
|
||||
|
||||
def db_exists(db):
|
||||
return psql('postgres', "select 1 from pg_database where datname=%s" % sql_lit(db)) == '1'
|
||||
|
||||
|
||||
def sql_lit(s):
|
||||
return "'" + str(s).replace("'", "''") + "'"
|
||||
|
||||
|
||||
def gather(inst):
|
||||
slug = inst['slug']
|
||||
state = systemctl('is-active', inst['unit']).stdout.strip() or 'unknown'
|
||||
ports = listening_ports()
|
||||
bk = backup_state(slug)
|
||||
d = dict(inst)
|
||||
d['service'] = state
|
||||
d['listening'] = bool(inst['port'] and inst['port'] in ports)
|
||||
d['http_code'] = http_code(inst['port'])
|
||||
d['db_exists'] = db_exists(inst['db'])
|
||||
d['modules'] = module_versions(inst['db']) if d['db_exists'] else []
|
||||
d['backup'] = bk
|
||||
d['data_bytes'] = du_bytes(inst['data_dir'])
|
||||
d['vhost'] = find_vhost(slug, inst['port'])
|
||||
d['frontend_dir'] = '/var/www/%s-frontend' % slug
|
||||
return d
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- salida
|
||||
def print_table(headers, rows):
|
||||
if not rows:
|
||||
print(' (sin datos)')
|
||||
return
|
||||
cols = len(headers)
|
||||
widths = [len(str(h)) for h in headers]
|
||||
norm = []
|
||||
for r in rows:
|
||||
r = ['' if c is None else str(c) for c in r]
|
||||
norm.append(r)
|
||||
for i in range(cols):
|
||||
widths[i] = max(widths[i], len(r[i]))
|
||||
def fmt(r):
|
||||
return ' '.join(r[i].ljust(widths[i]) for i in range(cols))
|
||||
print(fmt([str(h) for h in headers]))
|
||||
print('-' * (sum(widths) + 2 * (cols - 1)))
|
||||
for r in norm:
|
||||
print(fmt(r))
|
||||
|
||||
|
||||
def cmd_listar(args, insts):
|
||||
rows = []
|
||||
for d in insts:
|
||||
bk = d['backup']
|
||||
last = bk['last'] or '-'
|
||||
rows.append([
|
||||
d['slug'], d['unit'].replace('.service', ''), d['service'],
|
||||
d['port'] or '-', d['db'],
|
||||
('200' if d['http_code'] == 200 else (str(d['http_code']) if d['http_code'] else 'X')),
|
||||
(len(d['modules']) if d['db_exists'] else 'X'),
|
||||
(bk['last_mtime'] or '-'),
|
||||
human(bk['last_size']) if bk['count'] else '-',
|
||||
bk['count'],
|
||||
human(bk['dir_bytes']),
|
||||
human(d['data_bytes']),
|
||||
('si' if d['listening'] else 'no'),
|
||||
])
|
||||
headers = ['slug', 'unit', 'svc', 'puerto', 'db', 'http', 'mods',
|
||||
'ultimo backup', 'tam', 'n', 'backups', 'datadir', 'escucha']
|
||||
if args.json_global or args.json_sub:
|
||||
print(json.dumps(insts, default=str, indent=2))
|
||||
return
|
||||
print('Instancias SKEEN detectadas (%d):' % len(insts))
|
||||
print_table(headers, rows)
|
||||
print('\nRetención: %s diarios / %s semanales / %s mensuales | backups en %s'
|
||||
% (ENV.get('RETAIN_DAILY'), ENV.get('RETAIN_WEEKLY'),
|
||||
ENV.get('RETAIN_MONTHLY'), ENV['BACKUP_ROOT']))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_estado(args, insts):
|
||||
d = next((x for x in insts if x['slug'] == args.slug), None)
|
||||
if not d:
|
||||
print('ERROR: no existe la instancia %r. Detectadas: %s'
|
||||
% (args.slug, ', '.join(x['slug'] for x in insts)), file=sys.stderr)
|
||||
return 1
|
||||
if args.json_global or args.json_sub:
|
||||
print(json.dumps(d, default=str, indent=2))
|
||||
return 0
|
||||
bk = d['backup']
|
||||
print('Instancia: %s' % d['slug'])
|
||||
print('Unidad: %s (%s) -> %s' % (d['unit'], d['service'], d['unit_path']))
|
||||
print('Conf: %s' % d['conf'])
|
||||
print('Base datos: %s%s' % (d['db'], '' if d['db_exists'] else ' [NO EXISTE]'))
|
||||
print('Puerto HTTP: %s escuchando=%s' % (d['port'], 'si' if d['listening'] else 'no'))
|
||||
print('HTTP /web/login: %s' % (d['http_code'] if d['http_code'] else 'sin respuesta'))
|
||||
print('Log Odoo: %s' % d['logfile'])
|
||||
print('data_dir: %s (%s)' % (d['data_dir'] or '-', human(d['data_bytes'])))
|
||||
print('Vhost nginx: %s' % (d['vhost'] or '-'))
|
||||
print('Frontend: %s%s' % (d['frontend_dir'],
|
||||
'' if os.path.isdir(d['frontend_dir']) else ' [falta]'))
|
||||
print('Módulos skeen instalados: %d' % len(d['modules']))
|
||||
for n, v in d['modules']:
|
||||
print(' %-22s %s' % (n, v))
|
||||
print('Backups: %d archivos, %s en %s' % (bk['count'], human(bk['dir_bytes']), bk['dir']))
|
||||
print('Último: %s (%s, %s)' % (bk['last'] or '-', human(bk['last_size']), bk['last_mtime'] or '-'))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_version(args, insts):
|
||||
slugs = [d['slug'] for d in insts]
|
||||
data = {d['slug']: dict(d['modules']) for d in insts}
|
||||
if args.json_global or args.json_sub:
|
||||
print(json.dumps(data, indent=2))
|
||||
return 0
|
||||
print('Versiones de módulos skeen_* instalados por instancia:\n')
|
||||
names = sorted({n for d in insts for n, _ in d['modules']})
|
||||
rows = []
|
||||
for n in names:
|
||||
row = [n]
|
||||
for s in slugs:
|
||||
row.append(data[s].get(n, '-'))
|
||||
rows.append(row)
|
||||
print_table(['módulo'] + slugs, rows)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_uso(args, insts):
|
||||
disk = shutil.disk_usage('/')
|
||||
mem = {}
|
||||
try:
|
||||
with open('/proc/meminfo') as fh:
|
||||
for line in fh:
|
||||
k, v = line.split(':')
|
||||
mem[k] = int(v.strip().split()[0]) * 1024
|
||||
except OSError:
|
||||
pass
|
||||
load = os.getloadavg()
|
||||
ncpu = os.cpu_count() or 1
|
||||
procs = []
|
||||
p = run(['ps', '-eo', 'pid,pcpu,pmem,rss,etime,args', '--sort=-rss'])
|
||||
for line in p.stdout.splitlines()[1:]:
|
||||
if 'odoo-bin' in line and 'grep' not in line:
|
||||
pid, pcpu, pmem, rss, etime, rest = line.split(None, 5)
|
||||
m = re.search(r'-c\s+(\S+)', rest)
|
||||
procs.append({
|
||||
'pid': int(pid), 'cpu': float(pcpu), 'mem': float(pmem),
|
||||
'rss': int(rss) * 1024, 'uptime': etime,
|
||||
'conf': m.group(1) if m else '?',
|
||||
})
|
||||
data = {
|
||||
'host': os.uname().nodename,
|
||||
'disk': {'total': disk.total, 'used': disk.used, 'free': disk.free,
|
||||
'used_pct': round(disk.used * 100.0 / disk.total, 1)},
|
||||
'mem': {'total': mem.get('MemTotal'), 'available': mem.get('MemAvailable'),
|
||||
'used': (mem.get('MemTotal', 0) - mem.get('MemAvailable', 0))},
|
||||
'load': {'1m': load[0], '5m': load[1], '15m': load[2], 'ncpu': ncpu},
|
||||
'procesos_odoo': procs,
|
||||
}
|
||||
if args.json_global or args.json_sub:
|
||||
print(json.dumps(data, indent=2))
|
||||
return 0
|
||||
print('Host: %s (CPU: %d núcleos)' % (data['host'], ncpu))
|
||||
print('Disco /: %s usados de %s (%.1f%%), libres %s'
|
||||
% (human(disk.used), human(disk.total), data['disk']['used_pct'], human(disk.free)))
|
||||
print('RAM: %s usados de %s, disponibles %s'
|
||||
% (human(data['mem']['used']), human(data['mem']['total']), human(data['mem']['available'])))
|
||||
print('Carga: %.2f %.2f %.2f (1m/5m/15m)'
|
||||
% (load[0], load[1], load[2]))
|
||||
print('\nProcesos Odoo (por RSS):')
|
||||
print_table(['pid', 'cpu%', 'mem%', 'rss', 'uptime', 'conf'],
|
||||
[[p['pid'], '%.1f' % p['cpu'], '%.1f' % p['mem'],
|
||||
human(p['rss']), p['uptime'], p['conf']] for p in procs])
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_instalacion(args, insts):
|
||||
d = next((x for x in insts if x['slug'] == args.slug), None)
|
||||
if not d:
|
||||
print('ERROR: no existe la instancia %r' % args.slug, file=sys.stderr)
|
||||
return 1
|
||||
frontend_ok = os.path.isfile(os.path.join(d['frontend_dir'], 'index.html'))
|
||||
brand_dirs = glob.glob(os.path.join(d['frontend_dir'], 'brand', '*')) + \
|
||||
glob.glob(os.path.join(d['frontend_dir'], 'skeen-brand', '*'))
|
||||
brand_param = psql(d['db'], "select value from ir_config_parameter where key='skeen.clinic.brand'") if d['db_exists'] else ''
|
||||
checks = [
|
||||
('DB existe', d['db_exists'], d['db']),
|
||||
('Unit activa', d['service'] == 'active', '%s = %s' % (d['unit'], d['service'])),
|
||||
('Conf Odoo', os.path.isfile(d['conf']), d['conf']),
|
||||
('Vhost nginx', bool(d['vhost']), d['vhost'] or 'no encontrado'),
|
||||
('Puerto escuchando', d['listening'], str(d['port'])),
|
||||
('HTTP /web/login', d['http_code'] == 200, str(d['http_code'] or 'sin respuesta')),
|
||||
('Frontend desplegado', frontend_ok, d['frontend_dir']),
|
||||
('Marca (archivos)', bool(brand_dirs), '%d archivos' % len(brand_dirs)),
|
||||
('Marca (param DB)', bool(brand_param), (brand_param[:60] + '...') if len(brand_param) > 60 else brand_param),
|
||||
]
|
||||
if args.json_global or args.json_sub:
|
||||
print(json.dumps({'slug': d['slug'],
|
||||
'checks': [{'check': c, 'ok': ok, 'detalle': det} for c, ok, det in checks],
|
||||
'completa': all(ok for _, ok, _ in checks)}, indent=2))
|
||||
return 0
|
||||
print('Validación de instalación — %s\n' % d['slug'])
|
||||
rows = [[('OK' if ok else 'FALTA'), c, det] for c, ok, det in checks]
|
||||
print_table(['estado', 'check', 'detalle'], rows)
|
||||
faltan = [c for c, ok, _ in checks if not ok]
|
||||
print('\n%s' % ('Instalación COMPLETA.' if not faltan
|
||||
else 'Falta: ' + ', '.join(faltan)))
|
||||
return 0 if not faltan else 1
|
||||
|
||||
|
||||
def cmd_logs(args, insts):
|
||||
d = next((x for x in insts if x['slug'] == args.slug), None)
|
||||
if not d:
|
||||
print('ERROR: no existe la instancia %r' % args.slug, file=sys.stderr)
|
||||
return 1
|
||||
n = args.n
|
||||
def tail(path, label):
|
||||
print('\n=== %s (%s) — últimas %d líneas ===' % (label, path, n))
|
||||
if not os.path.isfile(path):
|
||||
print(' (no existe)')
|
||||
return []
|
||||
p = run(['tail', '-n', str(n), path])
|
||||
print(p.stdout.rstrip() or ' (vacío)')
|
||||
return p.stdout.rstrip().splitlines()
|
||||
if args.json_global or args.json_sub:
|
||||
data = {}
|
||||
for key, path in (('odoo', d['logfile']), ('backup', ENV.get('LOG_FILE'))):
|
||||
data[key] = run(['tail', '-n', str(n), path]).stdout.rstrip().splitlines() \
|
||||
if os.path.isfile(path) else []
|
||||
print(json.dumps(data, indent=2))
|
||||
return 0
|
||||
tail(d['logfile'], 'Log Odoo')
|
||||
tail(ENV.get('LOG_FILE'), 'Log de backups')
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_credenciales(args, insts):
|
||||
d = next((x for x in insts if x['slug'] == args.slug), None)
|
||||
if not d:
|
||||
print('ERROR: no existe la instancia %r' % args.slug, file=sys.stderr)
|
||||
return 1
|
||||
pats = [os.path.join(ENV['BACKUP_ROOT'], '%s-credenciales-*.txt' % args.slug)]
|
||||
files = sorted(f for p in pats for f in glob.glob(p))
|
||||
if args.json_global or args.json_sub:
|
||||
print(json.dumps({'slug': args.slug, 'archivos': files,
|
||||
'contenido': [open(f).read() for f in files] if args.mostrar else None},
|
||||
indent=2))
|
||||
return 0
|
||||
print('Ruta de credenciales de %s (generadas por el provisioning):' % args.slug)
|
||||
if not files:
|
||||
print(' %s/<slug>-credenciales-<fecha>.txt -> no encontrado' % ENV['BACKUP_ROOT'])
|
||||
print(' El provisioning las guarda en %s con chmod 600.' % ENV['BACKUP_ROOT'])
|
||||
print(' Para volver a generarlas: infra/provision/provision_clinica.py (o revisar el alta manual).')
|
||||
return 0
|
||||
for f in files:
|
||||
print(' %s (%s, %s)' % (f, human(os.path.getsize(f)),
|
||||
datetime.fromtimestamp(os.path.getmtime(f)).isoformat(timespec='seconds')))
|
||||
if not args.mostrar:
|
||||
print('\n(usa --mostrar para imprimir el contenido; nunca se muestran passwords sin ese flag)')
|
||||
return 0
|
||||
for f in files:
|
||||
print('\n----- %s -----' % f)
|
||||
print(open(f, encoding='utf-8', errors='replace').read().rstrip())
|
||||
return 0
|
||||
|
||||
|
||||
# ------------------------------------------------------------- acciones (mutan)
|
||||
def lock_is_busy():
|
||||
import fcntl
|
||||
try:
|
||||
fh = open(ENV['LOCK_FILE'], 'a+')
|
||||
except OSError:
|
||||
return False
|
||||
try:
|
||||
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
fcntl.flock(fh, fcntl.LOCK_UN)
|
||||
return False
|
||||
except OSError:
|
||||
return True
|
||||
finally:
|
||||
fh.close()
|
||||
|
||||
|
||||
def cmd_backup(args, insts):
|
||||
d = next((x for x in insts if x['slug'] == args.slug), None)
|
||||
if not d:
|
||||
print('ERROR: no existe la instancia %r' % args.slug, file=sys.stderr)
|
||||
return 1
|
||||
if not args.yes:
|
||||
print('Acción con CAMBIO DE ESTADO: se hará un backup de %s (db %s).' % (d['slug'], d['db']))
|
||||
print('Repite con --yes para confirmar.')
|
||||
return 2
|
||||
if lock_is_busy():
|
||||
print('ERROR: hay otro backup en curso (lock %s). Espera y reintenta.' % ENV['LOCK_FILE'],
|
||||
file=sys.stderr)
|
||||
return 3
|
||||
cmd = [BACKUP_SCRIPT, '--instancia', d['slug']]
|
||||
if args.verificar:
|
||||
cmd.append('--verificar')
|
||||
print('Ejecutando: %s' % ' '.join(cmd))
|
||||
return subprocess.call(cmd)
|
||||
|
||||
|
||||
def cmd_reiniciar(args, insts):
|
||||
d = next((x for x in insts if x['slug'] == args.slug), None)
|
||||
if not d:
|
||||
print('ERROR: no existe la instancia %r' % args.slug, file=sys.stderr)
|
||||
return 1
|
||||
if not args.yes:
|
||||
print('Acción con CAMBIO DE ESTADO: reiniciar %s (%s).' % (d['unit'], d['slug']))
|
||||
print('ADVERTENCIA: reiniciar INTERRUMPE el servicio mientras arranca.')
|
||||
print('Repite con --yes para confirmar.')
|
||||
return 2
|
||||
if lock_is_busy():
|
||||
print('ERROR: hay un backup en curso (lock %s); no se reinicia.' % ENV['LOCK_FILE'],
|
||||
file=sys.stderr)
|
||||
return 3
|
||||
print('ADVERTENCIA: reiniciar %s interrumpe el servicio de %s.' % (d['unit'], d['slug']))
|
||||
print('Ejecutando: systemctl restart %s' % d['unit'])
|
||||
return subprocess.call(['systemctl', 'restart', d['unit']])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- main
|
||||
def build_parser():
|
||||
p = argparse.ArgumentParser(description='Panel/CLI multi-instancia SKEEN (read-only por defecto).')
|
||||
p.add_argument('--json', action='store_true', dest='json_global', help='salida JSON')
|
||||
sub = p.add_subparsers(dest='cmd')
|
||||
|
||||
def add(name, fn, **kw):
|
||||
sp = sub.add_parser(name, **kw)
|
||||
sp.add_argument('--json', action='store_true', dest='json_sub', help=argparse.SUPPRESS)
|
||||
sp.set_defaults(func=fn)
|
||||
return sp
|
||||
|
||||
add('listar', cmd_listar, help='tabla de instancias (default)')
|
||||
sp = add('estado', cmd_estado, help='detalle de una instancia'); sp.add_argument('slug')
|
||||
add('version', cmd_version, help='versiones de módulos skeen_*')
|
||||
add('uso', cmd_uso, help='disco/RAM/CPU del host y procesos Odoo')
|
||||
sp = add('instalacion', cmd_instalacion, help='valida que una instancia esté completa'); sp.add_argument('slug')
|
||||
sp = add('logs', cmd_logs, help='tail de logs de una instancia'); sp.add_argument('slug')
|
||||
sp.add_argument('-n', type=int, default=20, help='número de líneas (default 20)')
|
||||
sp = add('credenciales', cmd_credenciales, help='ruta de credenciales')
|
||||
sp.add_argument('slug'); sp.add_argument('--mostrar', action='store_true',
|
||||
help='imprime el contenido (incluye passwords)')
|
||||
sp = add('backup', cmd_backup, help='lanza un backup (requiere --yes)')
|
||||
sp.add_argument('slug'); sp.add_argument('--verificar', action='store_true')
|
||||
sp.add_argument('--yes', action='store_true')
|
||||
sp = add('reiniciar', cmd_reiniciar, help='reinicia la instancia (requiere --yes)')
|
||||
sp.add_argument('slug'); sp.add_argument('--yes', action='store_true')
|
||||
|
||||
p.set_defaults(func=cmd_listar)
|
||||
return p
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
for attr in ('json_sub',):
|
||||
if not hasattr(args, attr):
|
||||
setattr(args, attr, False)
|
||||
insts = discover()
|
||||
if not insts:
|
||||
print('ERROR: no se detectó ninguna instancia (unidades odoo-*.service).', file=sys.stderr)
|
||||
return 1
|
||||
insts = [gather(i) for i in insts]
|
||||
return args.func(args, insts)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user