diff --git a/sigi/apps/servicos/__init__.py b/sigi/apps/servicos/__init__.py index e69de29..b86b8b7 100644 --- a/sigi/apps/servicos/__init__.py +++ b/sigi/apps/servicos/__init__.py @@ -0,0 +1,19 @@ +def generate_instance_name(orgao): + import re + from sigi.apps.utils import to_ascii + + # Orgao deve ser uma instância de sigi.apps.casas.models.Orgao # + if orgao.tipo.sigla == "CM": + return ( + re.sub("\W+", "", to_ascii(orgao.municipio.nome)).lower() + + "-" + + orgao.municipio.uf.sigla.lower() + ) + elif orgao.tipo.sigla == "CT": + return "cl-df" + elif orgao.tipo.sigla == "AL": + return f"al-{orgao.municipio.uf.sigla.lower()}" + elif orgao.tipo.sigla in ["CD", "SF"]: + return re.sub("\W+", "", to_ascii(orgao.nome)).lower() + else: + return f"{orgao.tipo.sigla.lower()}-{orgao.municipio.uf.sigla.lower()}" diff --git a/sigi/apps/servicos/admin.py b/sigi/apps/servicos/admin.py index 4b7766b..9eb7b24 100644 --- a/sigi/apps/servicos/admin.py +++ b/sigi/apps/servicos/admin.py @@ -69,6 +69,7 @@ class ServicoAdmin(CartExportMixin, admin.ModelAdmin): ] list_display = ( "tipo_servico", + "versao", "casa_legislativa", "get_uf", "hospedagem_interlegis", @@ -83,8 +84,10 @@ class ServicoAdmin(CartExportMixin, admin.ModelAdmin): fields = [ "casa_legislativa", "tipo_servico", + "versao", "url", "hospedagem_interlegis", + "instancia", "data_ativacao", "data_alteracao", "data_desativacao", diff --git a/sigi/apps/servicos/jobs/daily/sincroniza_rancher.py b/sigi/apps/servicos/jobs/daily/sincroniza_rancher.py new file mode 100644 index 0000000..33919eb --- /dev/null +++ b/sigi/apps/servicos/jobs/daily/sincroniza_rancher.py @@ -0,0 +1,204 @@ +import datetime +import docutils.core +import json +from pathlib import Path +from django.conf import settings +from django.core.mail import mail_admins +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 +from sigi.apps.servicos.models import Servico, TipoServico +from sigi.apps.casas.models import Orgao + + +class Job(DailyJob): + help = _("Sincronização dos Serviços SEIT na infraestrutura") + _nomes_gerados = None + _errors = {} + _infos = {} + + def execute(self): + print( + _( + "Sincroniza os serviços SEIT a partir da infraestrutura." + f" Início: {datetime.datetime.now(): %d/%m/%Y %H:%M:%S}" + ) + ) + self._nomes_gerados = { + generate_instance_name(o): o + for o in Orgao.objects.filter(tipo__legislativo=True) + } + print(f"\t{len(self._nomes_gerados)} órgãos que podem ter instâncias.") + + for tipo in TipoServico.objects.filter(modo="H").exclude( + tipo_rancher="" + ): + print( + _( + f"\tProcessando {tipo.nome}." + f" Início: {datetime.datetime.now():%H:%M:%S}." + ), + end="", + ) + self.process(tipo) + print(f" Término: {datetime.datetime.now():%H:%M:%S}.") + + print("Relatório final:\n================") + self.report() + + print(_(f"Término: {datetime.datetime.now(): %d/%m/%Y %H:%M:%S}")) + + def process(self, tipo): + NAO_CONSTA = "*não-consta-no-rancher*" + self._errors[tipo] = [] + self._infos[tipo] = [] + + file_path = settings.HOSPEDAGEM_PATH / tipo.arquivo_rancher + if not file_path.exists() or not file_path.is_file(): + self._errors[tipo].append(_(f"Arquivo {file_path} não encontado.")) + return + + with open(file_path, "r") as f: + json_data = json.load(f) + + portais = [ + item + for item in json_data["items"] + if item["spec"]["chart"]["metadata"]["name"] == tipo.tipo_rancher + ] + + encontrados = 0 + novos = 0 + desativados = 0 + + self._infos[tipo].append( + _(f"{len(portais)} {tipo.nome} encontrados no Rancher") + ) + + # Atualiza portais existentes e cria novos # + for p in portais: + iname = 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 + self._errors[tipo].append( + _( + f"Instância {iname} de {tipo.nome} sem URL no " + "rancher" + ) + ) + + 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 + self._errors[tipo].append( + _(f"Instância {iname} de {tipo.nome} sem URL no rancher") + ) + + try: + portal = Servico.objects.get(instancia=iname, tipo_servico=tipo) + encontrados += 1 + except Servico.DoesNotExist: + if iname in self._nomes_gerados: + orgao = self._nomes_gerados[iname] + portal = Servico( + casa_legislativa=orgao, + tipo_servico=tipo, + instancia=iname, + data_ativacao=p["spec"]["info"]["firstDeployed"][:10], + ) + self._infos[tipo].append( + _( + f"Criada instância {iname} de {tipo.nome} para " + f"{orgao.nome} ({orgao.municipio.uf.sigla})" + ) + ) + novos += 1 + else: + self._errors[tipo].append( + _( + f"{iname} ({hostname}) não parece pertencer a " + "nenhum órgão." + ) + ) + continue + # atualiza o serviço no SIGI + portal.versao = ( + p["spec"]["values"]["image"]["tag"] + if "image" in p["spec"]["values"] + else "" + ) + if NAO_CONSTA in hostname: + portal.url = "" + else: + portal.url = f"https://{hostname}/" + portal.hospedagem_interlegis = True + portal.save() + + # 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._infos[tipo].append( + f"{portal.instancia} ({portal.url}) de " + f"{portal.casa_legislativa.nome} desativado pois não " + "foi encontrado no Rancher." + ) + desativados += 1 + + self._infos[tipo].append( + _(f"{encontrados} {tipo.nome} do Rancher encontrados no SIGI") + ) + self._infos[tipo].append( + _(f"{novos} novos {tipo.nome} criados no SIGI") + ) + self._infos[tipo].append( + _(f"{desativados} {tipo.nome} desativados no SIGI") + ) + + def report(self): + rst = render_to_string( + "servicos/emails/report_sincroniza_rancher.rst", + { + "erros": self._errors, + "infos": self._infos, + "title": _("Resultado da sincronização do SIGI com o Rancher"), + }, + ) + html = docutils.core.publish_string( + rst, + writer_name="html5", + settings_overrides={ + "input_encoding": "unicode", + "output_encoding": "unicode", + }, + ) + mail_admins( + subject=self.help, + message=rst, + html_message=html, + fail_silently=True, + ) + print(rst) diff --git a/sigi/apps/servicos/migrations/0014_servico_instancia_servico_versao_and_more.py b/sigi/apps/servicos/migrations/0014_servico_instancia_servico_versao_and_more.py new file mode 100644 index 0000000..aa3a9f5 --- /dev/null +++ b/sigi/apps/servicos/migrations/0014_servico_instancia_servico_versao_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.1.2 on 2022-10-20 15:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("servicos", "0013_alter_logservico_data_alter_servico_data_ativacao"), + ] + + operations = [ + migrations.AddField( + model_name="servico", + name="instancia", + field=models.CharField( + blank=True, max_length=100, verbose_name="nome da instância" + ), + ), + migrations.AddField( + model_name="servico", + name="versao", + field=models.CharField(blank=True, max_length=20, verbose_name="versão"), + ), + migrations.AddField( + model_name="tiposervico", + name="arquivo_rancher", + field=models.CharField( + blank=True, + max_length=100, + verbose_name="nome do arquivo gerado no rancher", + ), + ), + migrations.AddField( + model_name="tiposervico", + name="prefixo_padrao", + field=models.CharField(blank=True, max_length=20), + ), + migrations.AddField( + model_name="tiposervico", + name="spec_rancher", + field=models.CharField( + blank=True, max_length=100, verbose_name="spec do serviço no Rancher" + ), + ), + migrations.AddField( + model_name="tiposervico", + name="tipo_rancher", + field=models.CharField( + blank=True, max_length=100, verbose_name="tipo de objeto no Rancher" + ), + ), + ] diff --git a/sigi/apps/servicos/migrations/0015_nomeia_instancias.py b/sigi/apps/servicos/migrations/0015_nomeia_instancias.py new file mode 100644 index 0000000..7aac613 --- /dev/null +++ b/sigi/apps/servicos/migrations/0015_nomeia_instancias.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.1 on 2022-10-03 21:00 + +from django.db import migrations +from sigi.apps.servicos import generate_instance_name + + +def instance_names_fw(apps, schema_editor): + Servico = apps.get_model("servicos", "Servico") + for s in Servico.objects.filter(data_desativacao=None): + s.instancia = generate_instance_name(s.casa_legislativa) + s.save() + + +def instance_names_rw(apps, schema_editor): + Servico = apps.get_model("servicos", "Servico") + Servico.objects.all().update(instancia="") + + +class Migration(migrations.Migration): + + dependencies = [ + ("servicos", "0014_servico_instancia_servico_versao_and_more"), + ] + + operations = [ + migrations.RunPython(instance_names_fw, instance_names_rw), + ] diff --git a/sigi/apps/servicos/models.py b/sigi/apps/servicos/models.py index c3d26f2..c83067a 100644 --- a/sigi/apps/servicos/models.py +++ b/sigi/apps/servicos/models.py @@ -1,3 +1,4 @@ +import black from django.utils import timezone from django.db import models from sigi.apps.casas.models import Orgao, Funcionario @@ -18,6 +19,16 @@ class TipoServico(models.Model): modo = models.CharField( _("modo de prestação do serviço"), max_length=1, choices=MODO_CHOICES ) + 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 ) @@ -63,9 +74,13 @@ class Servico(models.Model): TipoServico, on_delete=models.PROTECT, verbose_name=_("tipo de serviço") ) url = models.URLField(_("URL do serviço"), blank=True) + versao = models.CharField(_("versão"), max_length=20, blank=True) hospedagem_interlegis = models.BooleanField( _("Hospedagem no Interlegis?"), default=False ) + instancia = models.CharField( + _("nome da instância"), max_length=100, blank=True + ) data_ativacao = models.DateField( _("Data de ativação"), default=timezone.localdate ) diff --git a/sigi/apps/servicos/templates/servicos/emails/report_sincroniza_rancher.rst b/sigi/apps/servicos/templates/servicos/emails/report_sincroniza_rancher.rst new file mode 100644 index 0000000..815fce0 --- /dev/null +++ b/sigi/apps/servicos/templates/servicos/emails/report_sincroniza_rancher.rst @@ -0,0 +1,34 @@ +{% extends 'emails/base_email.rst' %} +{% load i18n %} + +{% block content %} + +{% trans "Resultado da sincronização dos dados de serviços do SIGI com as instâncias instaladas no Rancher." %} + +* {% trans "Data/hora de execução" %}: {% now 'SHORT_DATETIME_FORMAT' %} + + +**{% trans "ERROS ENCONTRADOS" %}** +===================== +{% for tipo, mensagens in erros.items %} + **{{ tipo.nome|upper }} - {{ tipo.sigla|upper }}** + {% for m in mensagens %} + * {{ m }} + {% endfor %} +{% empty %} + *{% trans "Nenhum erro encontrado" %}* +{% endfor %} + + +**{% trans "INFORMAÇÕES ADICIONAIS" %}** +========================== +{% for tipo, mensagens in infos.items %} + {{ tipo.nome|upper }} - {{ tipo.sigla|upper }} + {% for m in mensagens %} + * {{ m }} + {% endfor %} +{% empty %} + *{% trans "Nenhuma informação adicional gerada" %}* +{% endfor %} + +{% endblock content %} \ No newline at end of file diff --git a/sigi/settings.py b/sigi/settings.py index d1ed521..9d70a70 100644 --- a/sigi/settings.py +++ b/sigi/settings.py @@ -253,3 +253,5 @@ TINYMCE_DEFAULT_CONFIG = { # SIGI specific settings MENU_FILE = BASE_DIR / "menu_conf.yaml" +HOSPEDAGEM_PATH = Path(env("HOSPEDAGEM_PATH", default="/tmp/HOSP/")) +REGISTRO_PATH = Path(env("REGISTRO_PATH", default="/tmp/DNS/"))