diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cc9656e..a82b7f1 100644 --- a/requirements/requirements.txt +++ b/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 diff --git a/sigi/apps/servicos/exceptions.py b/sigi/apps/servicos/exceptions.py new file mode 100644 index 0000000..7381bb1 --- /dev/null +++ b/sigi/apps/servicos/exceptions.py @@ -0,0 +1,2 @@ +class MultipleServicesReturned(Exception): + pass diff --git a/sigi/apps/servicos/jobs/daily/atualiza_dns.py b/sigi/apps/servicos/jobs/daily/atualiza_dns.py new file mode 100644 index 0000000..46b6947 --- /dev/null +++ b/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" + ), + ) diff --git a/sigi/apps/servicos/jobs/daily/atualiza_servicos.py b/sigi/apps/servicos/jobs/daily/atualiza_servicos.py new file mode 100644 index 0000000..491c815 --- /dev/null +++ b/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" + ), + ) diff --git a/sigi/apps/servicos/jobs/daily/sincroniza_dns.py b/sigi/apps/servicos/jobs/daily/sincroniza_dns.py deleted file mode 100644 index 2217464..0000000 --- a/sigi/apps/servicos/jobs/daily/sincroniza_dns.py +++ /dev/null @@ -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, - ) diff --git a/sigi/apps/servicos/jobs/daily/sincroniza_rancher.py b/sigi/apps/servicos/jobs/daily/sincroniza_rancher.py deleted file mode 100644 index ca4505a..0000000 --- a/sigi/apps/servicos/jobs/daily/sincroniza_rancher.py +++ /dev/null @@ -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") diff --git a/sigi/apps/servicos/migrations/0023_remove_tiposervico_arquivo_rancher_and_more.py b/sigi/apps/servicos/migrations/0023_remove_tiposervico_arquivo_rancher_and_more.py new file mode 100644 index 0000000..8479397 --- /dev/null +++ b/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", + ), + ] diff --git a/sigi/apps/servicos/migrations/0024_auto_20260807_1218.py b/sigi/apps/servicos/migrations/0024_auto_20260807_1218.py new file mode 100644 index 0000000..8115f65 --- /dev/null +++ b/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), + ] diff --git a/sigi/apps/servicos/migrations/0025_auto_20260812_1130.py b/sigi/apps/servicos/migrations/0025_auto_20260812_1130.py new file mode 100644 index 0000000..872aa48 --- /dev/null +++ b/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)] diff --git a/sigi/apps/servicos/models.py b/sigi/apps/servicos/models.py index 04f56be..b8b4ca1 100644 --- a/sigi/apps/servicos/models.py +++ b/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,
" 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") diff --git a/sigi/apps/utils/management/jobs.py b/sigi/apps/utils/management/jobs.py index 0d64449..602e621 100644 --- a/sigi/apps/utils/management/jobs.py +++ b/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, ) diff --git a/sigi/settings.py b/sigi/settings.py index 5fc25ef..ea56400 100644 --- a/sigi/settings.py +++ b/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