Browse Source

Nova forma de processar serviços SEIT

master
Sesóstris Vieira 5 days ago
parent
commit
0c652c011e
  1. 1
      requirements/requirements.txt
  2. 2
      sigi/apps/servicos/exceptions.py
  3. 389
      sigi/apps/servicos/jobs/daily/atualiza_dns.py
  4. 474
      sigi/apps/servicos/jobs/daily/atualiza_servicos.py
  5. 335
      sigi/apps/servicos/jobs/daily/sincroniza_dns.py
  6. 248
      sigi/apps/servicos/jobs/daily/sincroniza_rancher.py
  7. 21
      sigi/apps/servicos/migrations/0023_remove_tiposervico_arquivo_rancher_and_more.py
  8. 23
      sigi/apps/servicos/migrations/0024_auto_20260807_1218.py
  9. 32
      sigi/apps/servicos/migrations/0025_auto_20260812_1130.py
  10. 15
      sigi/apps/servicos/models.py
  11. 12
      sigi/apps/utils/management/jobs.py
  12. 9
      sigi/settings.py

1
requirements/requirements.txt

@ -32,6 +32,7 @@ PyYAML==6.0.3
requests==2.33.1
tzdata==2026.1
uritemplate==4.2.0
url-normalize==3.0.0
weasyprint==68.1
xlsxwriter==3.2.9
djbs-theme @ git+https://github.com/interlegis/djbs-theme.git@v1.0.8

2
sigi/apps/servicos/exceptions.py

@ -0,0 +1,2 @@
class MultipleServicesReturned(Exception):
pass

389
sigi/apps/servicos/jobs/daily/atualiza_dns.py

@ -0,0 +1,389 @@
import requests
import sys
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext as _
from django_extensions.management.jobs import DailyJob
from url_normalize import url_normalize
from sigi.apps.casas.models import Orgao, TipoOrgao
from sigi.apps.contatos.models import UnidadeFederativa
from sigi.apps.servicos import generate_instance_name, nomeia_instancias
from sigi.apps.servicos.exceptions import MultipleServicesReturned
from sigi.apps.servicos.models import Servico, LogServico, TipoServico
from sigi.apps.utils.management.jobs import AdminJobMixin
class Job(AdminJobMixin, DailyJob):
help = _("Atualiza registros de DNS")
nomes_gerados = None
dados = None
counter = 0
ignorados = 0
erros = 0
updates = 0
desativados = 0
novos = 0
def __init__(self):
super().__init__()
self.nomes_gerados = {
generate_instance_name(o): o
for o in Orgao.objects.filter(tipo__legislativo=True)
}
def execute(self):
if not self._retrieve_json_data():
print(_("Processo abortado!"), file=sys.stderr)
return
registros_ativos = Servico.objects.filter(
tipo_servico__modo=TipoServico.MODO_REGISTRO, data_desativacao=None
)
nomeia_instancias(
servicos=registros_ativos.filter(instancia=""),
user=self.get_sys_user(),
)
registros_ativos.update(flag_confirmado=False)
total = len(self.dados)
self.erros = 0
self.updates = 0
self.desativados = 0
self.novos = 0
print(
"\n\n",
_("Processando {total} registros recebidos").format(total=total),
"\n\n",
)
for rec in self.get_dados():
tipo_servico = self.get_tipo_servico(rec)
if not tipo_servico:
self.erros += 1
continue
try:
servico = self.get_registro(tipo_servico, rec)
except MultipleServicesReturned as e:
print("* ", str(e), file=sys.sys.stderr)
self.erros += 1
continue
if servico:
self.atualiza_registro(servico, rec)
continue
# Tenta criar o registro
orgao = self.get_orgao(rec)
if not orgao:
self.erros += 1
continue
self.cria_registro(rec, tipo_servico, orgao)
self.novos += 1
# Desativar todos que não foram confirmados
nao_confirmados = registros_ativos.filter(flag_confirmado=False)
self.desativados += self.bulk_desativa(nao_confirmados)
print("\n\n", _("TOTAIS"))
print("------", "\n")
print(
_(" * registros recebidos do webservice: {total}").format(
total=total
)
)
print(
_(" * registros ignorados..............: {ignorados}").format(
ignorados=self.ignorados
)
)
print(
_(" * registros com erro...............: {erros}").format(
erros=self.erros
)
)
print(
_(" * atualizações realizadas..........: {updates}").format(
updates=self.updates
)
)
print(
_(" * registros desativados............: {desativados}").format(
desativados=self.desativados
)
)
print(
_(" * novos registros criados..........: {novos}").format(
novos=self.novos
)
)
def _retrieve_json_data(self):
if (not hasattr(settings, "REGISTRO_PATH")) or (
not settings.REGISTRO_PATH
):
print(
_(
"Falta a configuração da URL de acesso aos registros de "
"DNS instalados"
),
file=sys.stderr,
)
return False
print(
_("Buscando dados no webservice {wsentry}...").format(
wsentry=settings.REGISTRO_PATH
),
)
try:
response = requests.get(settings.REGISTRO_PATH)
except Exception as e:
print(
_("Ocorreu um erro ao acessar {url}: {error}").format(
url=settings.REGISTRO_PATH, error=str(e)
),
file=sys.stderr,
)
return False
if response.status_code != 200:
print(
_(
"Problemas na resposta do webservice de DNS: "
"{code} - {reason}"
).format(code=response.status_code, reason=response.reason),
file=sys.stderr,
)
return False
if "json" not in response.headers["content-type"]:
print(
_("Tipo de conteúdo não é JSON: {contenttype}").format(
contenttype=response.headers["content-type"]
),
file=sys.stderr,
)
return False
result = response.json()
if result["tipo"] != "DNS":
print(
_("Tipo de resultado inesperado: {tipo}").format(
tipo=result["tipo"]
),
file=sys.stderr,
)
return False
if result["status"] != "ok":
print(
_(
"O webservice respondeu com status {status}: logs: {logs}"
).format(status=result["status"], logs=result["logs"]),
file=sys.stderr,
)
return False
self.dados = result["result"]["dominios"]
return True
def get_dados(self):
if self.dados is None:
if not self._retrieve_json_data():
return
self.ignorados = 0
self.counter = 0
for record in self.dados:
self.counter += 1
if (
"interlegis" in record["url"].lower()
or "interlegis" in record["orgao"].lower()
):
self.ignorados += 1
continue
yield record
def get_tipo_servico(self, rec):
try:
tipo_servico = TipoServico.objects.get(tipo_rancher=rec["tipo"])
except TipoServico.DoesNotExist:
print(
"* ",
_(
"Erro ao processar {counter}º registro. Tipo de "
"registro desconhecido: {tipo}. Registro: {r}"
).format(counter=self.counter, tipo=rec["tipo"], r=rec),
file=sys.stderr,
)
return None
if tipo_servico.modo != TipoServico.MODO_REGISTRO:
print(
"* ",
_(
"O {counter}º registro de DNS {rec} indica um tipo de "
"serviço que não é de registro de DNS."
).format(counter=self.counter, rec=rec),
file=sys.stderr,
)
return None
return tipo_servico
def get_orgao(self, rec):
# tenta achar pelo órgão dado no registro
if rec["orgao"] in self.nomes_gerados:
return self.nomes_gerados[rec["orgao"]]
# Senão, vamos buscar pelo nome do domínio na URL
partes = rec["url"].split(".")
# A UF deve ser o 2º nível e o domínio, o último
uf = partes[-3]
dominio = partes[0].replace("https://", "")
if not UnidadeFederativa.objects.filter(sigla=uf).exists():
print(
"* ",
_(
"Impossível identificar o órgão dono do registro {url}, "
"no {counter}º registro: {rec}"
).format(url=rec["url"], counter=self.counter, rec=rec),
file=sys.stderr,
)
return None
nome_orgao = f"{dominio}-{uf}"
if nome_orgao in self.nomes_gerados:
return self.nomes_gerados[nome_orgao]
return None
def get_registro(self, tipo_servico, rec):
servicos = Servico.objects.filter(
tipo_servico=tipo_servico,
instancia=rec["orgao"],
data_desativacao=None,
).order_by("-data_ativacao")
if not servicos:
# Nenhum registro ativo, podemos criar um
return None
if len(servicos) == 1:
# Um único registro ativo, deve ser esse
return servicos[0]
# Mais de um registro ativo para este órgão.
# Não tem como decidir qual atualizar
raise MultipleServicesReturned(
_(
"Existem {qty} registros ativos para o órgão {orgao}. "
"Registro {r}"
).format(qty=len(servicos), orgao=rec["orgao"], r=rec)
)
def atualiza_registro(self, registro, rec):
old = registro.url
new = rec["url"]
if old != new:
registro.url = new
self.updates += 1
log = _(
"A URL do {tipo} de {orgao} atualizado de {old} para {new}"
).format(
tipo=registro.tipo_servico.sigla,
orgao=registro.casa_legislativa.nome,
old=old,
new=new,
)
print("* ", log)
registro.logservico_set.create(
descricao=_("URL atualizada"),
data=timezone.localdate(),
log=log,
)
self.admin_log_change(registro, log)
registro.flag_confirmado = True
registro.save()
def cria_registro(self, rec, tipo_servico, orgao):
registro = Servico(
casa_legislativa=orgao,
tipo_servico=tipo_servico,
url=rec["url"],
hospedagem_interlegis=rec["hospedagem"] == "Interlegis",
instancia=rec["orgao"],
data_ativacao=timezone.localdate(),
flag_confirmado=True,
)
registro.save()
registro.logservico_set.create(
descricao=_("Serviço criado no SEIT"),
data=timezone.localdate(),
log=_(
"Servico criado no SEIT e atualizado no SIGI "
"automaticamente pelo processo de CRON"
),
)
self.admin_log_addition(
registro,
_(
"Servico criado no SEIT e atualizado no SIGI "
"automaticamente pelo processo de CRON"
),
)
print(
"* ",
_("{servico} criado para {orgao}").format(
servico=registro.tipo_servico.nome,
orgao=registro.casa_legislativa.nome,
),
)
def bulk_desativa(self, registros):
log_list = [
LogServico(
servico=s,
descricao=_("Serviço desativado no SEIT"),
data=timezone.localdate(),
log=_(
"Desativado automaticamente pois não foi encontrado na "
"Infraestrutura do Interlegis"
),
)
for s in registros
]
LogServico.objects.bulk_create(log_list)
self.admin_log_change(
registros,
_(
"Desativado automaticamente pois não foi encontrado na "
"Infraestrutura do Interlegis"
),
)
print(
*[
_(
"* {servico} de {orgao} desativado porque não consta nos "
"dados do webservice do SEIT\n"
).format(
servico=s.tipo_servico.nome, orgao=s.casa_legislativa.nome
)
for s in registros
],
)
return registros.update(
data_desativacao=timezone.localdate(),
motivo_desativacao=_(
"Desativado automaticamente pois não foi encontrado na "
"Infraestrutura do Interlegis"
),
)

474
sigi/apps/servicos/jobs/daily/atualiza_servicos.py

@ -0,0 +1,474 @@
from datetime import datetime
import requests
import sys
from django.conf import settings
from django.utils import timezone
from django.utils.formats import localize
from django.utils.translation import gettext as _
from django_extensions.management.jobs import DailyJob
from url_normalize import url_normalize
from sigi.apps.servicos import generate_instance_name, nomeia_instancias
from sigi.apps.servicos.exceptions import MultipleServicesReturned
from sigi.apps.servicos.models import Servico, LogServico, TipoServico
from sigi.apps.casas.models import Orgao, TipoOrgao
from sigi.apps.utils import to_ascii
from sigi.apps.utils.management.jobs import AdminJobMixin
class Job(AdminJobMixin, DailyJob):
help = _("Sincronização dos Serviços SEIT na infraestrutura")
UPDATE_NOTHING = None
UPDATE_DEACTIVATED = 0
UPDATE_UPDATED = 1
UPDATABLE_FIELDS = (
("url", "url"),
("versao", "version"),
)
nomes_gerados = None
dados = None
counter = 0
ignorados = 0
erros = 0
updates = 0
desativados = 0
novos = 0
def __init__(self):
super().__init__()
self.nomes_gerados = {
generate_instance_name(o): o
for o in Orgao.objects.filter(tipo__legislativo=True)
}
def execute(self):
if not self._retrieve_json_data():
print(_("Processo abortado!"), file=sys.stderr)
return
hospedagens_ativas = Servico.objects.filter(
tipo_servico__modo=TipoServico.MODO_HOSPEDAGEM,
data_desativacao=None,
).exclude(tipo_servico__tipo_rancher="")
nomeia_instancias(
servicos=hospedagens_ativas.filter(instancia=""),
user=self.get_sys_user(),
)
hospedagens_ativas.update(flag_confirmado=False)
total = len(self.dados)
self.erros = 0
self.updates = 0
self.desativados = 0
self.novos = 0
print(
"\n\n",
_("Processando {total} registros recebidos").format(total=total),
"\n\n",
)
for rec in self.get_dados():
tipo_servico = self.get_tipo_servico(rec)
if not tipo_servico:
self.erros += 1
continue
try:
servico = self.get_servico(tipo_servico, rec)
except MultipleServicesReturned as e:
print("* ", str(e), file=sys.stderr)
self.erros += 1
continue
if servico:
self.atualiza_servico(servico, rec)
continue
# Tenta criar o serviço
orgao = self.get_orgao(rec)
if not orgao:
self.erros += 1
continue
self.cria_servico(rec, tipo_servico, orgao)
self.novos += 1
# Desativar todos que não foram confirmados
nao_confirmados = hospedagens_ativas.filter(flag_confirmado=False)
self.desativados += self.bulk_desativa(nao_confirmados)
print("\n\n", _("TOTAIS"))
print("------", "\n\n")
print(" * registros recebidos do webservice: ", total)
print(" * registros ignorados..............: ", self.ignorados)
print(" * registros com erro...............: ", self.erros)
print(" * atualizações realizadas..........: ", self.updates)
print(" * serviços desativados.............: ", self.desativados)
print(" * novos serviços criados...........: ", self.novos)
def _retrieve_json_data(self):
if (not hasattr(settings, "HOSPEDAGEM_PATH")) or (
not settings.HOSPEDAGEM_PATH
):
print(
_(
"Falta a configuração da URL de acesso aos serviços "
"instalados"
),
file=sys.stderr,
)
return False
print(
_("Buscando dados no webservice {wsentry}...").format(
wsentry=settings.HOSPEDAGEM_PATH
),
)
try:
response = requests.get(settings.HOSPEDAGEM_PATH)
except Exception as e:
print(
_("Ocorreu um erro ao acessar {url}: {error}").format(
url=settings.HOSPEDAGEM_PATH,
error=str(e),
),
file=sys.stderr,
)
return False
if response.status_code != 200:
print(
_(
"Problemas na resposta do webservice de serviços: "
"{code} - {reason}"
).format(code=response.status_code, reason=response.reason),
file=sys.stderr,
)
return False
if not "json" in response.headers["content-type"]:
print(
_("Tipo de conteúdo não é JSON: {contenttype}").format(
contenttype=response.headers["content-type"]
),
file=sys.stderr,
)
return False
result = response.json()
if result["tipo"] != "SERVICES":
print(
_("Tipo de resultado inesperado: {tipo}").format(
tipo=result["tipo"]
),
file=sys.stderr,
)
return False
if result["status"] != "ok":
print(
_(
"O webservice respondeu com status {status}: logs: {logs}"
).format(status=result["status"], logs=result["logs"]),
file=sys.stderr,
)
return False
self.dados = result["result"]["servicos"]
return True
def get_dados(self):
if self.dados is None:
if not self._retrieve_json_data():
return
self.ignorados = 0
self.counter = 0
for r in self.dados:
self.counter += 1
record = r.copy()
if "interlegis.leg.br" in record["url"]:
self.ignorados += 1
continue
record["url"] = url_normalize(record["url"])
record["creationDate"] = (
datetime.strptime(record["creationDate"], "%d/%m/%Y").date()
if "creationDate" in record and record["creationDate"] != ""
else None
)
record["suspendedDate"] = (
datetime.strptime(record["suspendedDate"], "%d/%m/%Y").date()
if "suspendedDate" in record and record["suspendedDate"] != ""
else None
)
yield record
def get_tipo_servico(self, rec):
# Verificar se existe sub-serviço
if rec["namespace"].count("-") > 1:
subservico = rec["namespace"].split("-")[0]
try:
tipo_servico = TipoServico.objects.get(tipo_rancher=subservico)
return tipo_servico
except TipoServico.DoesNotExist:
# Tenta encontrar o serviço principal
pass
try:
tipo_servico = TipoServico.objects.get(tipo_rancher=rec["tipo"])
except TipoServico.DoesNotExist:
print(
"* ",
_(
"Erro ao processar {counter}º registro. Tipo de "
"serviço desconhecido: {tipo}. Registro: {r}"
).format(counter=self.counter, tipo=rec["tipo"], r=rec),
file=sys.stderr,
)
return False
return tipo_servico
def get_orgao(self, rec):
# Tenta achar pelo namespace, que é mais canônico
namespace = rec["namespace"]
if namespace.count("-") > 1:
namespace = "-".join(namespace.split("-")[1:])
if namespace in self.nomes_gerados:
return self.nomes_gerados[namespace]
# Senão, vamos buscar pelo tipo de órgão e nome
try:
tipo, nome_uf = rec["orgao"].split(" - ")
if nome_uf.count("-") > 1:
subproduto, nome, uf = nome_uf.split("-")
else:
nome, uf = nome_uf.split("-")
except ValueError:
print(
"* ",
_(
"Nome do órgão fora do padrão no {counter}º registro: {r}"
).format(counter=self.counter, r=rec),
file=sys.stderr,
)
return False
tipo = to_ascii(tipo).lower()
nome = to_ascii(nome).lower()
uf = to_ascii(uf).lower()
cidade_uf = "-".join([nome, uf])
try:
tipo_orgao = TipoOrgao.objects.get(nome__unaccent__icontains=tipo)
except TipoOrgao.DoesNotExist:
print(
"* ",
_(
"Tipo de órgão desconhecido no {counter}º "
"registro. Nome do órgão: {orgao}, registro: {r}"
).format(counter=self.counter, orgao=rec["orgao"], r=rec),
file=sys.stderr,
)
return False
if cidade_uf in self.nomes_gerados:
orgao = self.nomes_gerados[cidade_uf]
if orgao.tipo != tipo_orgao:
print(
"* ",
_(
"Encontrado um órgão para o municipio "
"{municipio} - {uf}, mas o tipo de órgão difere do "
"recebido do webservice. Registro: {rec}"
).format(municipio=nome, uf=uf, rec=rec),
file=sys.stderr,
)
return False
return orgao
return False
def get_servico(self, tipo_servico, rec):
servicos = Servico.objects.filter(
tipo_servico=tipo_servico,
instancia=rec["namespace"],
data_desativacao=None,
).order_by("-data_ativacao")
# Nenhum serviço ativo nesse namespace. Podemos criar um novo
if not servicos:
return None
if len(servicos) == 1:
# Um único serviço ativo, deve ser esse
return servicos[0]
# Mais de um serviço ativo para este namespace.
# Não tem como decidir qual atualizar
raise MultipleServicesReturned(
_(
"Existem {qty} serviços ativos para o namespace {namespace}. "
"Registro {r}"
).format(qty=len(servicos), namespace=rec["namespace"], r=rec)
)
def atualiza_servico(self, servico, rec):
log_tit = []
log_txt = []
updated = False
for attrname, recfield in self.UPDATABLE_FIELDS:
old = getattr(servico, attrname)
new = rec[recfield]
if old != new:
updated = True
log_tit.append(
_(f"Mudança de {attrname}").format(
attrname=attrname.upper()
)
)
log_txt.append(
_(
"A {attrname} do serviço foi alterada na "
"infraestrutura de {old} para {new}."
).format(attrname=attrname, old=old, new=new)
)
setattr(servico, attrname, new)
if (
rec["status"] == "suspenso" and servico.data_desativacao is None
) or (
rec["suspendedDate"] is not None
and servico.data_desativacao != rec["suspendedDate"]
):
servico.data_desativacao = rec["suspendedDate"]
servico.motivo_desativacao = _(
"Serviço suspenso no SEIT e atualizado automaticamente "
"pelo processo de cron."
)
log_tit.append(_("Serviço suspenso no SEIT"))
log_txt.append(
_(
"Serviço suspenso no SEIT e atualizado automaticamente "
"pelo processo de cron."
)
)
print(
"* ",
_("{servico} de {orgao} desativado").format(
servico=servico.tipo_servico.nome,
orgao=servico.casa_legislativa.nome,
),
)
self.desativados += 1
elif updated:
self.updates += 1
print(
"* ",
_("{servico} de {orgao} atualizado").format(
servico=servico.tipo_servico.nome,
orgao=servico.casa_legislativa.nome,
),
)
servico.flag_confirmado = True
servico.save()
if log_tit:
log_tit = ", ".join(log_tit)
log_txt = "\n\n".join(log_txt)
servico.logservico_set.create(
descricao=log_tit,
data=timezone.localdate(),
log=log_txt,
)
self.admin_log_change(servico, log_txt)
def cria_servico(self, rec, tipo_servico, orgao):
servico = Servico(
casa_legislativa=orgao,
tipo_servico=tipo_servico,
url=rec["url"],
versao=rec["version"],
hospedagem_interlegis=True,
instancia=rec["namespace"],
data_ativacao=rec["creationDate"],
data_desativacao=rec["suspendedDate"],
motivo_desativacao=(
"" if rec["suspendedDate"] is None else _("Suspenso no SEIT")
),
flag_confirmado=True,
)
servico.save()
servico.logservico_set.create(
descricao=_("Serviço criado no SEIT"),
data=timezone.localdate(),
log=_(
"Servico criado no SEIT e atualizado no SIGI "
"automaticamente pelo processo de CRON"
),
)
self.admin_log_addition(
servico,
_(
"Servico criado no SEIT e atualizado no SIGI "
"automaticamente pelo processo de CRON"
),
)
print(
"* ",
_("{servico} criado para {orgao}").format(
servico=servico.tipo_servico.nome,
orgao=servico.casa_legislativa.nome,
),
)
def bulk_desativa(self, servicos):
log_list = [
LogServico(
servico=s,
descricao=_("Serviço desativado no SEIT"),
data=timezone.localdate(),
log=_(
"Desativado automaticamente pois não foi encontrado na "
"Infraestrutura do Interlegis"
),
)
for s in servicos
]
LogServico.objects.bulk_create(log_list)
self.admin_log_change(
servicos,
_(
"Desativado automaticamente pois não foi encontrado na "
"Infraestrutura do Interlegis"
),
)
print(
*[
_(
"* {servico} de {orgao} desativado porque não consta nos "
"dados do webservice do SEIT\n"
).format(
servico=s.tipo_servico.nome, orgao=s.casa_legislativa.nome
)
for s in servicos
],
)
return servicos.update(
data_desativacao=timezone.localdate(),
motivo_desativacao=_(
"Desativado automaticamente pois não foi encontrado na "
"Infraestrutura do Interlegis"
),
)

335
sigi/apps/servicos/jobs/daily/sincroniza_dns.py

@ -1,335 +0,0 @@
import json
import shutil
import sys
from django.conf import settings
from django.db.models import Q
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext as _
from django_extensions.management.jobs import DailyJob
from sigi.apps.servicos import generate_instance_name, nomeia_instancias
from sigi.apps.servicos.models import Servico, TipoServico
from sigi.apps.casas.models import Orgao
from sigi.apps.contatos.models import UnidadeFederativa
from sigi.apps.utils.management.jobs import AdminJobMixin
LOG_GERAL = _("Mensagens gerais")
IGNORES = ["_psl", "k8s", "www.", "sapl.", "addr.arpa"]
get_iname = lambda d: "-".join(d.split(".")[:-2])
get_sigla_serv = lambda d: "".join(d.split(".")[-2:]).upper()
get_sigla_uf = lambda d: "".join(d.split(".")[-3:-2]).upper()
def get_log_entry():
return {
"sumario": {
"total": 0,
"novos": 0,
"atualizados": 0,
"desativados": 0,
"ignorados": 0,
},
"infos": [],
"erros": [],
}
class Job(AdminJobMixin, DailyJob):
help = _("Sincronização dos registros de DNS da infraestrutura")
nomes_gerados = None
report_data = {}
def execute(self):
self.report_data[LOG_GERAL] = get_log_entry()
if (
not settings.REGISTRO_PATH.exists()
or not settings.REGISTRO_PATH.is_dir()
):
self.error(_(f"Arquivos de DNS não encontrados."))
return
self.nomes_gerados = {
generate_instance_name(o): o
for o in Orgao.objects.filter(tipo__legislativo=True)
}
Servico.objects.filter(
tipo_servico__modo="R", data_desativacao=None
).update(flag_confirmado=False)
nomeia_instancias(
servicos=Servico.objects.filter(
tipo_servico__modo="R", data_desativacao=None, instancia=""
),
user=self.sys_user,
)
# Remove arquivo ZONES, desnecessário para este processo #
zones_file = settings.REGISTRO_PATH / "ZONES"
if zones_file.exists() and zones_file.is_file():
zones_file.unlink()
for uf in UnidadeFederativa.objects.all():
self.report_data[uf] = get_log_entry()
self.processa_uf(uf)
# self.processa_zones()
self.processa_files()
try:
shutil.rmtree(settings.REGISTRO_PATH)
except Exception as e:
self.error(_(f"Erro ao excluir diretório {settings.REGISTRO_PATH}"))
print(
render_to_string(
"servicos/emails/report_sincroniza_dns.rst",
context={"report_data": self.report_data},
)
)
if any([len(d.erros) > 0 for uf, d in self.report_data.items()]):
print("* EXISTEM ERROS A SEREM ANALISADOS *", file=sys.stderr)
def processa_rec(self, dns_rec, log_entry=LOG_GERAL):
dominio = dns_rec["name"][:-1]
nivel = dominio.count(".") + 1
iname = get_iname(dominio)
sigla_srv = get_sigla_serv(dominio)
if any([i in dominio for i in IGNORES]):
# Ignorar esses registros sem fazer log #
return
try:
tipo = TipoServico.objects.get(sigla=sigla_srv, modo="R")
except TipoServico.DoesNotExist:
self.log_ignore(
dominio,
_("não coincide com nenhum tipo de serviço de registro SEIT"),
log_entry,
)
return
if log_entry == LOG_GERAL:
try:
log_entry = UnidadeFederativa.objects.get(
sigla=get_sigla_uf(dominio)
)
except:
pass
detail_file = settings.REGISTRO_PATH / f"{dominio}."
hospedado_interlegis = detail_file.exists() and detail_file.is_file()
filtro_base = Q(instancia=iname) | Q(url=dominio)
if iname in self.nomes_gerados:
filtro_base = filtro_base | Q(
casa_legislativa=self.nomes_gerados[iname]
)
filtro_base = filtro_base & Q(tipo_servico=tipo)
servico = None
novo = False
try:
servico = Servico.objects.get(
filtro_base & Q(data_desativacao=None)
)
except Servico.MultipleObjectsReturned:
self.log_ignore(
dominio,
_(
"existe mais de um registro no SIGI para a instância "
f"{iname}, domínio {dominio}"
),
log_entry,
)
return
except Servico.DoesNotExist:
# Tenta encontrar um registro desativado para esta instância #
servico = Servico.objects.filter(
filtro_base & ~Q(data_desativacao=None)
).first()
if servico is not None:
# Reativa o servico #
self.log_reativa(servico)
servico.data_desativacao = None
servico.motivo_desativacao = ""
servico.instancia = iname
self.admin_log_change(servico, _("Reativado pelo DNS Rancher"))
else:
# Tenta criar o registro #
if iname in self.nomes_gerados:
orgao = self.nomes_gerados[iname]
log_entry = orgao.municipio.uf
servico = Servico(
casa_legislativa=orgao,
tipo_servico=tipo,
url=dominio,
instancia=iname,
hospedagem_interlegis=hospedado_interlegis,
data_ativacao=timezone.localdate(),
flag_confirmado=True,
resultado_verificacao="N", # Não verificado
)
servico.save()
novo = True
self.log_novo(servico)
self.admin_log_addition(servico, "Criado pelo DNS Rancher")
if servico is None:
if nivel > 3:
# Loga registro não encontrado apenas para 4º+ nível #
self.log_ignore(
dominio,
_("não parece pertencer a nenhum órgão"),
log_entry,
)
elif not novo:
# atualiza o serviço no SIGI
updates = []
if servico.url != dominio:
updates.append(_(f"Url de '{servico.url}' para '{dominio}'"))
if servico.instancia != iname:
updates.append(
_(f"Instância de '{servico.instancia}' para '{iname}'")
)
if servico.hospedagem_interlegis != hospedado_interlegis:
updates.append(
"Veio para hospedagem no Interlegis"
if hospedado_interlegis
else "Passou a ser delegado"
)
servico.url = dominio
servico.instancia = iname
servico.hospedagem_interlegis = hospedado_interlegis
servico.flag_confirmado = True
servico.save()
if updates:
self.log_update(servico)
self.admin_log_change(
servico,
"Atualizado pelo DNS Rancher: " + ", ".join(updates),
)
def processa_uf(self, uf):
file_path = settings.REGISTRO_PATH / f"{uf.sigla.lower()}.leg.br."
if not file_path.exists() or not file_path.is_file():
self.error(_(f"Arquivo {file_path} não encontado."), uf)
return
registros = json.loads(file_path.read_text())["rrsets"]
self.report_data[uf]["sumario"]["total"] = len(registros)
# Atualiza registros existentes e cria novos #
for rec in registros:
dominio = rec["name"][:-1]
self.processa_rec(rec, uf)
# Remove arquivo de detalhe, se existente #
detail_file = settings.REGISTRO_PATH / f"{dominio}."
if (
detail_file != file_path
and detail_file.exists()
and detail_file.is_file()
):
detail_file.unlink()
# Remove arquivo da UF #
file_path.unlink()
# def processa_zones(self):
# zones_file = settings.REGISTRO_PATH / "ZONES"
# if not zones_file.exists() or not zones_file.is_file():
# self.error(
# _(
# f"Arquivo de zonas {zones_file} não encontrado ou "
# "não é arquivo"
# )
# )
# return
# data = json.loads(zones_file.read_text())
# for rec in data:
# dominio = rec["name"][:-1]
# self.processa_rec(rec)
# detail_file = settings.REGISTRO_PATH / f"{dominio}."
# if (
# detail_file != zones_file
# and detail_file.exists()
# and detail_file.is_file()
# ):
# detail_file.unlink()
# zones_file.unlink()
def processa_files(self):
file_list = list(settings.REGISTRO_PATH.iterdir())
self.report_data[LOG_GERAL]["sumario"]["total"] = len(file_list)
for file_path in file_list:
if not file_path.is_file():
self.report_data[LOG_GERAL]["sumario"]["total"] -= 1
continue
data = json.loads(file_path.read_text())
self.processa_rec(data)
file_path.unlink()
def remove_sigi(self):
# Desativa registros no SIGI que não estão no DNS #
for servico in Servico.objects.filter(
tipo_servico__modo="R",
data_desativacao=None,
flag_confirmado=False,
):
servico.data_desativacao = timezone.localdate()
servico.motivo_desativacao = _("Não encontrado no DNS")
servico.save()
self.log_remove(servico)
self.admin_log_change(
servico, _("Desativado: não encontrado no DNS Rancher")
)
def error(self, message, log_entry=LOG_GERAL):
self.report_data[log_entry]["erros"].append(message)
def info(self, message, log_entry=LOG_GERAL):
self.report_data[log_entry]["infos"].append(message)
def log_novo(self, srv):
orgao = srv.casa_legislativa
uf = orgao.municipio.uf
msg = _(
f"Criada instância {srv.instancia} de {srv.tipo_servico.nome} "
f"para {orgao.nome} ({uf.sigla})"
)
self.info(msg, uf)
self.report_data[uf]["sumario"]["novos"] += 1
def log_ignore(self, dominio, motivo, log_entry=LOG_GERAL):
self.error(_(f"Registro {dominio} ignorado pois {motivo}"), log_entry)
self.report_data[log_entry]["sumario"]["ignorados"] += 1
def log_update(self, srv):
uf = srv.casa_legislativa.municipio.uf
self.report_data[uf]["sumario"]["atualizados"] += 1
def log_reativa(self, srv):
orgao = srv.casa_legislativa
uf = orgao.municipio.uf
msg = _(
f"Instância {srv.instancia} de {srv.tipo_servico.nome} "
f"para {orgao.nome} ({uf.sigla}) reativada no SIGI"
)
self.report_data[uf]["sumario"]["atualizados"] += 1
self.info(msg, uf)
def log_remove(self, srv):
orgao = srv.casa_legislativa
uf = orgao.municipio.uf
self.report_data[uf]["sumario"]["desativados"] += 1
self.info(
_(
f"Registro {srv.tipo_servico.sigla} {srv.instancia} ({srv.url})"
f" de {orgao.nome} desativado pois não foi encontrado no DNS."
),
uf,
)

248
sigi/apps/servicos/jobs/daily/sincroniza_rancher.py

@ -1,248 +0,0 @@
import json
import shutil
import sys
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext as _
from django_extensions.management.jobs import DailyJob
from sigi.apps.servicos import generate_instance_name, nomeia_instancias
from sigi.apps.servicos.models import Servico, TipoServico
from sigi.apps.casas.models import Orgao
from sigi.apps.utils.management.jobs import AdminJobMixin
class Job(AdminJobMixin, DailyJob):
help = _("Sincronização dos Serviços SEIT na infraestrutura")
report_template = "servicos/emails/report_sincroniza_rancher.rst"
nomes_gerados = None
def execute(self):
self.nomes_gerados = {
generate_instance_name(o): o
for o in Orgao.objects.filter(tipo__legislativo=True)
}
for tipo in TipoServico.objects.filter(modo="H").exclude(
tipo_rancher=""
):
self.process(tipo)
try:
shutil.rmtree(settings.HOSPEDAGEM_PATH)
except Exception as e:
pass
def process(self, tipo):
nomeia_instancias(
servicos=Servico.objects.filter(
tipo_servico=tipo, data_desativacao=None, instancia=""
),
user=self.sys_user,
)
NAO_CONSTA = "*não-consta-no-rancher*"
file_path = settings.HOSPEDAGEM_PATH / tipo.arquivo_rancher
if not file_path.exists() or not file_path.is_file():
print(
f"{tipo}: Arquivo {file_path} não encontado.", file=sys.stderr
)
return
json_data = json.loads(file_path.read_text())
portais = [
item
for item in json_data["items"]
if item["kind"].lower() == "app"
and item["spec"]["chart"]["metadata"]["name"] == tipo.tipo_rancher
]
namespaces = [
item
for item in json_data["items"]
if item["kind"].lower() == "namespace"
]
encontrados = 0
novos = 0
desativados = 0
print(f"{len(portais)} {tipo.nome} encontrados no Rancher")
# Atualiza portais existentes e cria novos #
for p in portais:
namespace = p["metadata"]["namespace"]
name = p["metadata"]["name"]
if tipo.spec_rancher in p["spec"]["values"]:
if "hostname" in p["spec"]["values"][tipo.spec_rancher]:
hostname = p["spec"]["values"][tipo.spec_rancher][
"hostname"
]
elif "domain" in p["spec"]["values"][tipo.spec_rancher]:
hostname = p["spec"]["values"][tipo.spec_rancher]["domain"]
else:
hostname = NAO_CONSTA
print(
f"Instância {namespace} de {tipo.nome} sem URL no "
"rancher",
file=sys.stderr,
)
if "hostprefix" in p["spec"]["values"][tipo.spec_rancher]:
prefix = p["spec"]["values"][tipo.spec_rancher][
"hostprefix"
]
hostname = f"{prefix}.{hostname}"
elif tipo.prefixo_padrao != "":
hostname = f"{tipo.prefixo_padrao}.{hostname}"
else:
hostname = NAO_CONSTA
print(
f"Instância {namespace} de {tipo.nome} sem URL no rancher",
file=sys.stderr,
)
nova_versao = (
p["spec"]["values"]["image"]["tag"]
if "image" in p["spec"]["values"]
else ""
)
if NAO_CONSTA in hostname:
nova_url = ""
else:
nova_url = f"https://{hostname}/"
# Identificar registro de suspensão do namespace
suspenso = [
ns["metadata"]["annotations"]["suspenso"]
for ns in namespaces
if ns["metadata"]["name"] == namespace
and "suspenso" in ns["metadata"]["annotations"]
]
try:
portal = Servico.objects.get(
instancia=namespace,
tipo_servico=tipo,
data_desativacao=None,
)
encontrados += 1
except Servico.MultipleObjectsReturned:
print(
f"Existe mais de um registro ativo da instância "
f"{namespace} de {tipo}.",
file=sys.stderr,
)
continue
except Servico.DoesNotExist:
# Se a instância está suspensa, não precisa criar o registro
# no SIGI.
if suspenso:
continue
if (
namespace in self.nomes_gerados
or name in self.nomes_gerados
):
orgao = (
self.nomes_gerados[namespace]
if namespace in self.nomes_gerados
else self.nomes_gerados[name]
)
portal = Servico(
casa_legislativa=orgao,
tipo_servico=tipo,
instancia=namespace,
url=nova_url,
versao=nova_versao,
data_ativacao=p["spec"]["info"]["firstDeployed"][:10],
hospedagem_interlegis=True,
)
portal.save()
self.admin_log_addition(portal, "Criado no Rancher")
novos += 1
print(
f"Criada instância {namespace} de {tipo.nome} para "
f"{orgao.nome} ({orgao.municipio.uf.sigla})"
)
else:
print(
f"{namespace} ({hostname}) não parece pertencer a "
"nenhum órgão.",
file=sys.stderr,
)
continue
# se tem registro de suspensão do namespace
if suspenso:
# Desativar o portal no SIGI
apontamentos = ", ".join([f'"{s}"' for s in suspenso])
portal.data_desativacao = timezone.localdate()
portal.motivo_desativacao = (
"Suspenso no Rancher com os seguintes apontamentos:"
+ apontamentos
)
portal.save()
self.admin_log_change(portal, portal.motivo_desativacao)
print(
f"{portal.tipo_servico} em {portal.url} de "
f"{portal.casa_legislativa} suspenso no Rancher com os"
f"seguintes apontamentos: {apontamentos}"
)
# atualiza o serviço no SIGI
if (
nova_versao != portal.versao
or nova_url != portal.url
or not portal.hospedagem_interlegis
):
message = (
"Atualizado no Rancher: "
+ (
f"Versão: de '{portal.versao}' para '{nova_versao}' "
if portal.versao != nova_versao
else ""
)
+ (
f"Url: de '{portal.url}' para '{nova_url}' "
if portal.url != nova_url
else ""
)
+ (
"hospedagem interlegis"
if not portal.hospedagem_interlegis
else ""
)
)
portal.versao = nova_versao
portal.url = nova_url
portal.hospedagem_interlegis = True
portal.save()
self.admin_log_change(portal, message)
print(
f"{portal.tipo_servico} em {portal.url} de "
f"{portal.casa_legislativa} atualizado no Rancher: "
+ message
)
# Desativa portais registrados no SIGI que não estão no Rancher #
nomes_instancias = [p["metadata"]["name"] for p in portais]
for portal in Servico.objects.filter(
tipo_servico=tipo,
data_desativacao=None,
hospedagem_interlegis=True,
):
if (
portal.instancia == ""
or portal.instancia not in nomes_instancias
):
portal.data_desativacao = timezone.localdate()
portal.motivo_desativacao = _("Não encontrado no Rancher")
portal.save()
self.admin_log_change(portal, "Desativado no Rancher")
print(
f"{portal.instancia} ({portal.url}) de "
f"{portal.casa_legislativa.nome} desativado pois não "
"foi encontrado no Rancher."
)
desativados += 1
print(f"{encontrados} {tipo.nome} do Rancher encontrados no SIGI")
print(f"{novos} novos {tipo.nome} criados no SIGI")
print(f"{desativados} {tipo.nome} desativados no SIGI")

21
sigi/apps/servicos/migrations/0023_remove_tiposervico_arquivo_rancher_and_more.py

@ -0,0 +1,21 @@
# Generated by Django 6.0.4 on 2026-08-07 15:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("servicos", "0022_atualiza_mail_leg"),
]
operations = [
migrations.RemoveField(
model_name="tiposervico",
name="arquivo_rancher",
),
migrations.RemoveField(
model_name="tiposervico",
name="spec_rancher",
),
]

23
sigi/apps/servicos/migrations/0024_auto_20260807_1218.py

@ -0,0 +1,23 @@
# Generated by Django 6.0.4 on 2026-08-07 15:18
from url_normalize import url_normalize
from django.contrib.postgres.operations import UnaccentExtension
from django.db import migrations
def mormaliza_urls(apps, schema_editor):
Servico = apps.get_model("servicos", "Servico")
for s in Servico.objects.filter(tipo_servico__modo="H").exclude(url=""):
s.url = url_normalize(s.url)
s.save()
class Migration(migrations.Migration):
dependencies = [
("servicos", "0023_remove_tiposervico_arquivo_rancher_and_more"),
]
operations = [
UnaccentExtension(),
migrations.RunPython(mormaliza_urls, migrations.RunPython.noop),
]

32
sigi/apps/servicos/migrations/0025_auto_20260812_1130.py

@ -0,0 +1,32 @@
# Generated by Django 6.0.4 on 2026-08-12 14:30
from django.db import migrations
SIGLA_X_RANCHER = (
("GOVBR", "GOV.BR"),
("LEGBR", "LEG.BR"),
("DNSREV", "DNS reverso"),
)
def update_registros(apps, schema_editor):
TipoServico = apps.get_model("servicos", "TipoServico")
for sigla, tipo_rancher in SIGLA_X_RANCHER:
TipoServico.objects.filter(sigla=sigla).update(
tipo_rancher=tipo_rancher
)
def reverse_registros(apps, schema_editor):
TipoServico = apps.get_model("servicos", "TipoServico")
for sigla, tipo_rancher in SIGLA_X_RANCHER:
TipoServico.objects.filter(sigla=sigla).update(tipo_rancher="")
class Migration(migrations.Migration):
dependencies = [
("servicos", "0024_auto_20260807_1218"),
]
operations = [migrations.RunPython(update_registros, reverse_registros)]

15
sigi/apps/servicos/models.py

@ -6,7 +6,12 @@ from django.utils.translation import gettext as _
class TipoServico(models.Model):
MODO_CHOICES = (("H", _("Hospedagem")), ("R", _("Registro")))
MODO_HOSPEDAGEM = "H"
MODO_REGISTRO = "R"
MODO_CHOICES = (
(MODO_HOSPEDAGEM, _("Hospedagem")),
(MODO_REGISTRO, _("Registro")),
)
email_help = "Use a marcação {url} para incluir a URL do serviço,<br/>"
string_pesquisa_help = (
"Parâmetros da pesquisa para averiguar a data da "
@ -22,12 +27,6 @@ class TipoServico(models.Model):
tipo_rancher = models.CharField(
_("tipo de objeto no Rancher"), max_length=100, blank=True
)
spec_rancher = models.CharField(
_("spec do serviço no Rancher"), max_length=100, blank=True
)
arquivo_rancher = models.CharField(
_("nome do arquivo gerado no rancher"), max_length=100, blank=True
)
prefixo_padrao = models.CharField(max_length=20, blank=True)
string_pesquisa = models.TextField(
_("string de pesquisa"), blank=True, help_text=string_pesquisa_help
@ -373,7 +372,7 @@ class LogServico(models.Model):
log = models.TextField(_("Log da ação"))
def __str__(self):
return f"{self.descricao} ({self.data})"
return f"{self.descricao}"
class Meta:
verbose_name = _("Log do serviço")

12
sigi/apps/utils/management/jobs.py

@ -31,15 +31,17 @@ class AdminJobMixin:
pass
return self._sys_user
def _admin_log(self, object, action_flag, message=""):
def _admin_log(self, queryset_or_object, action_flag, message=""):
sys_user = self.get_sys_user()
if sys_user is None:
return # No admin log
LogEntry.objects.log_action(
try:
iter(queryset_or_object)
except TypeError:
queryset_or_object = [queryset_or_object]
LogEntry.objects.log_actions(
user_id=sys_user.id,
content_type_id=ContentType.objects.get_for_model(type(object)).pk,
object_id=object.pk,
object_repr=str(object),
queryset=queryset_or_object,
action_flag=action_flag,
change_message=message,
)

9
sigi/settings.py

@ -51,10 +51,11 @@ INSTALLED_APPS = [
"admin_auto_filters",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.postgres",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.staticfiles",
"django_extensions",
"rangefilter",
"job_controller",
@ -312,8 +313,8 @@ SPECTACULAR_SETTINGS = {
# SIGI specific settings
HOSPEDAGEM_PATH = Path(env("HOSPEDAGEM_PATH", default="/tmp/HOSP/"))
REGISTRO_PATH = Path(env("REGISTRO_PATH", default="/tmp/DNS/"))
HOSPEDAGEM_PATH = env("HOSPEDAGEM_PATH", default=None)
REGISTRO_PATH = env("REGISTRO_PATH", default=None)
# Integração com Moodle

Loading…
Cancel
Save