quinta-feira, 9 de junho de 2016

Logins

Com quase dois milhões de logins registrados no meu log, resolvi verificar em quais horários as pessoas estão mais alertas e erram menos suas senhas.

Os resultados foram surpreendentes.

Hora Erros/Sucessos Total de tentativas
00 10,48 7270
01 36,94 7551
02 97,12 7163
03 104,19 6522
04 440,07 6175
05 73,77 6281
06 8,46 6928
07 0,52 45142
08 0,42 169947
09 0,47 209327
10 0,47 223268
11 0,47 175897
12 0,47 79421
13 0,42 163371
14 0,45 200312
15 0,44 187376
16 0,43 164907
17 0,51 75282
18 0,83 27162
19 1,61 14589
20 2,12 12932
21 2,52 12246
22 2,77 11047
23 4,24 8708

As primeiras horas da manhã não são as melhores horas para trabalhar. Eu não faria testes numa usina nuclear nestas horas. Os 440 erros para cada sucesso às 4h da manhã são deveras surpreendentes. Imagino que as pessoas tentem uma vez ou outra e simplesmente desistam, resolvendo esperar o sol aparecer.

Excluindo os IPs suspeitos (e devidamente bloqueados), a situação melhora um tanto.

Hora Erros/Sucessos Total de tentativas
00 6,11 4504
01 23,01 4779
02 58,9 4373
03 59,19 3732
04 244,64 3439
05 41,07 3534
06 4,7 4173
07 0,43 42420
08 0,4 167186
09 0,45 206585
10 0,45 220532
11 0,44 173169
12 0,42 76697
13 0,4 160585
14 0,43 197555
15 0,42 184612
16 0,4 162143
17 0,45 72495
18 0,65 24442
19 1,13 11882
20 1,46 10194
21 1,73 9517
22 1,82 8274
23 2,56 5919

As melhores horas foram 8h, quando presumo que as pessoas estejam começando a jornada, e 13h, quando presumo que estejam voltando do almoço.

Visando encontrar algum sentido nos números da madrugada, excluí todas as linhas relativas a tentativas vindas de IPs dos quais nunca houve um login com sucesso.

Hora Erros/Sucessos Total de tentativas
00 0,81 1155
01 4,54 1108
02 6,23 528
03 0,72 107
04 2,64 51
05 1,07 174
06 0,26 929
07 0,32 39324
08 0,37 164102
09 0,42 203523
10 0,42 216503
11 0,41 169326
12 0,35 73287
13 0,35 156826
14 0,39 193020
15 0,38 180017
16 0,36 157655
17 0,37 68708
18 0,41 20924
19 0,49 8317
20 0,57 6521
21 0,54 5365
22 0,49 4368
23 0,45 2426

Com isto, os números da madrugada revelam que há muitos engraçadinhos tentando entrar onde não deviam.

quarta-feira, 1 de junho de 2016

Ajustando tablespaces com facilidade

Uma tarefa recorrente na manutenção de um banco de dados Oracle é o de colocar objetos nos tablespaces corretos. Para facilitar o trabalho, recorri ao metadados. A rotina abaixo procura todas as tabelas, todos os índices e todos os LOBs de um schema e verifica se estão nos tablespaces indicados.

create or replace procedure 
  ajustar_tablespaces(p_schema varchar2, 
                      p_table_ts varchar2, 
                      p_index_ts varchar2, 
                      p_lob_ts varchar2) is
begin

  --Indices
  for r in 
   (select 'alter index '||p_schema||'.'|| index_name 
         ||' rebuild tablespace '||p_index_ts cmd 
    from all_indexes
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_index_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --Tabelas
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move tablespace '||p_table_ts cmd 
    from all_tables 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_table_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --LOBs
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move lob('||column_name||') store as (TABLESPACE '||p_lob_ts||')' cmd 
    from all_lobs 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_lob_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;
  
  --Reconstroi os indices
  for r in 
   (select 'alter index '||p_schema||'.'||index_name
         ||' rebuild' cmd  
    from all_indexes 
    where owner=p_schema 
      and status='UNUSABLE') 
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

end;
Então, para corrigir os tablespaces dos objetos de um schema, eu poderia fazer uma chamada como esta:

  exec ajustar_tablespaces('INFO', 'ts_data01', 'ts_index02', 'ts_lobs');

Os comandos são sendo impressos para que se possa acompanhar o andamento do trabalho, principalmente se os objetos forem grandes.

Abaixo, o mesmo código como um bloco a fim de facilitar execuções ad-hoc.

declare
  p_schema varchar2(100):='ABXY';
  p_table_ts varchar2(100):='DTBS1'; 
  p_index_ts varchar2(100):='ITBS1'; 
  p_lob_ts varchar2(100):='LTBS1';
begin

  --Indices
  for r in 
   (select 'alter index '||p_schema||'.'|| index_name 
         ||' rebuild tablespace '||p_index_ts cmd 
    from all_indexes
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_index_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --Tabelas
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move tablespace '||p_table_ts cmd 
    from all_tables 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_table_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --LOBs
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move lob('||column_name||') store as (TABLESPACE '||p_lob_ts||')' cmd 
    from all_lobs 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_lob_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;
  
  --Reconstroi os indices
  for r in 
   (select 'alter index '||p_schema||'.'||index_name
         ||' rebuild' cmd  
    from all_indexes 
    where owner=p_schema 
      and status='UNUSABLE') 
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

end;

sexta-feira, 27 de maio de 2016

Flashback de código no Oracle

Depois de uma tentativas desastrada de parcialmente atualizar uma package, precisei usar o flashback para recuperar o código original. Isso é fácil no Oracle:

  select text 
  from all_source as of timestamp sysdate -1/24 --uma hora
  where owner=:owner and name=:package_name and type='PACKAGE BODY'

Infelizmente, o SQLDeveloper exporta o texto com linhas adicionais. Contornar isso com um pouco de Perl é simples:

  perl -ne "print if !/^$/" package.sql > package_sem_linhas_vazias.sql

Esta linha de Perl imprime todas as linhas do arquivo (último parâmetro) que tiverem alguma coisa (/^$/ é verdadeiro para linhas vazias).

sábado, 13 de fevereiro de 2016

Solução caseira para segurança doméstica

As soluções de segurança que tenho visto são caras, então resolvi desenvolver uma solução caseira. Decidi que com um Raspberry Pi, uma câmera barata e um pouco de código, poderia fazer algo tão bom quanto sistemas dez vezes mais caros.

O mais importante é que o sistema deveria enviar notificações para o celular. O Whatsapp não permite robôs, então recorri ao Telegram.

Desenvolvi usando um Raspberry Pi B, mas o sistema final roda num Raspberry Pi A+ (que é ainda menor e mais barato). Acredito que funcionaria igualmente bem num Raspberry Pi Zero (que custa apenas US$5).

A ideia do projeto é bastante simples: a câmera, quando ativada por movimento, copia imagens para o Raspberry Pi e este, por sua vez, as encaminha para uma conta do Telegram. O Telegram entrega as fotos para o meu celular.

Se o código da câmera fosse aberto, ou ela oferecesse alguma API, seria possível que ela falasse diretamente com o Telegram. Ponto negativo para o código fechado.

Ingredientes
  • Um Raspberry Pi B (512MB de RAM, 700MHz de CPU e custa ~US$25);
  • Uma câmera D-Link 930LB (custa menos de R$200,00);
  • Uma conta no Telegram (Whatsapp não aceita robôs);
  • Um cartão de memória SDHC de 16GB (um de 8GB já resolveria);
  • Um pouco de Perl.
Passos

1. Gravar Raspbian no cartão.

2. Instalar um servidor de ftp (sudo apt-get install vsftpd).

3. Configurar o servidor de ftp (/etc/vsftpd.conf).

É preciso habilitar a possibilidade de escrever e de fazer login com contas locais:


local_enable=YES
write_enable=YES


4. Configurar um ramdisk (para não desgastar o cartão de memória, se o número de escritas fugir ao controle e porque é mais rápido).

É preciso adicionar esta linha ao arquivo /etc/fstab:


tmpfs  /home/pi/tmp  tmpfs  nodev,nosuid,size=1M 0 0


Esse comando cria um espaço de 1MB que fica montado no diretório tmp/ dentro do home do usuário pi, mas poderia ser qualquer outro lugar. Os arquivos não passam de 50KB, então essa área poderia ser muito menor.

5. Configurar a câmera.

Configurei a câmera para fazer o ftp da imagem quando for detectado um movimento (usei sensibilidade de 30%, porque ela envia muitas imagens com mais que isso).

O nome do arquivo indiquei como DCS-930LB e isso acaba resultando num arquivo JPEG chamado DCS-930LB.jpg.

6. Criar uma conta no Telegram.

É preciso procurar a conta @BotFather e conversar. Ele dá todas as instruções.

7. Programar em Perl (essa é a melhor parte).

É preciso instalar algumas bibliotecas, o curl, o cpanm e o módulo WWW::Telegram::BotAPI:

sudo apt-get install libnet-ssleay-perl libio-socket-ssl-perl --fix-missing
sudo apt-get install curl
sudo curl -L http://cpanmin.us | perl - --sudo App::cpanminus
sudo cpanm WWW::Telegram::BotAPI

Usei um modelo de robô e fiz algumas adaptações.

Em primeiro lugar, criei ums lista das contas autorizadas (para que estranhos não brinquem com meu robô):


my %chat_ids=(
  123456789=>'forinti',
  987654321=>'derpina'
);


Depois, mudei o loop principal para descansar 5s a cada iteração, verificar se a imagem mudou (pela hora de criação), e enviar a imagem se algo novo surgiu.


my $paused=0;
my $last_update=get_last_update();
while(1) {
  sleep(5);
  my $update=get_last_update();
  if(!$paused && $update>$last_update) {
    for my $id (keys %chat_ids) {
      $api->sendPhoto({
         chat_id=>$id, 
         photo=>{file=>'/home/pi/tmp/DCS-930LB.jpg'}
      });
    } 
    $last_update=$update;
  }
  ...


Finalmente, criei comandos para recuperar a última imagem (/li), parar o envio (/pause) e reiniciar o envio (/go).

sub get_last_update {
  return (stat('/home/pi/tmp/DCS-930LB.jpg'))[9];
}

sub get_last_image {
  return {
    method=>'sendPhoto',
    photo=>{ file=> '/home/pi/tmp/DCS-930LB.jpg' }
  };
}

sub pause {
  $paused=1;
  return 'Parou. Por enquanto...';
}

sub go {
  $paused=0;
  return 'Reiniciou.';
}


Resultado

Consegui produzir um sistema de segurança bastante simples e barato. Ele consome pouquíssima energia (menos de R$10,00 para operar um ano inteiro) e não precisa ser monitorado já que envia alertas. Por usar pouca energia, poderia funcionar ligado a uma bateria ou a um painel solar. Se conectasse a uma rede de celular, seria realmente resiliente.

terça-feira, 19 de janeiro de 2016

A linha mais comprida de um arquivo

Eu precisava descobrir quantas colunas tinha a linha mais comprida de um arquivo. O único porém é que o arquivo é muito grande.

Primeiro, tentei um pouco de shell.

while read line; do echo -n "$line" | wc -c; done< arquivao  | sort | uniq -c

Isso deveria apresentar o número de linhas com determinado comprimento e o número de ocorrências de cada comprimento (não há muita variação nos comprimentos das linhas, embora haja muitas linhas).

Infelizmente, não tive paciência de deixar esta pequena solução terminar. Resolvi averiguar se o Perl seria mais rápido.

perl -MData::Dumper -n -e '$n{length($_)}++; END {print Dumper(%n)}' arquivao


A solução é bem simples e muito mais rápida que a primeira. Com os parâmeteros -n e -e, o bloco de código é aplicado a cada linha do arquivo (último parâmetro). Cada tamanho de linha vira uma chave no hash %n e é incrementada a cada ocorrência. No fim, uso o módulo Data::Dumper para apresentar o que há dentro de %n.

sexta-feira, 15 de janeiro de 2016

Gotas de otimização Oracle

Há dois problemas de SQL comuns que vejo, com frequência, sendo resolvidos com complexidades e ineficiências desnecessárias.

O primeiro é o de precisar o número de registros de uma consulta junto com os dados. A solução ineficiente é a de executar a consulta duas vezes; usando count() primeiro e buscando os dados depois. As funções analíticas são pouco conhecidas ainda, infelizmente.


select count(1)
from pessoas;

select nome
from pessoas;


Usando a versão analítica de count(), as duas consultas podem ser unidas.

select nome, count(1) over ()
from pessoas


E ganha-se uma coluna com o total de linhas.

O segundo problema é o de retirar acentos. Já vi soluções com múltiplos ifs, com case, e até com muitos replace() aninhados.

A minha solução preferida é muito simples e fácil de ler:

select nome, translate(nome,
                'ÀÁÃÂÄÈÉÊËÙÚÛÜÌÍÎÏÒÓÔÕÖÇÑàáãâäèéêëùúûüìíîïòóôõöçñ',
                'AAAAAEEEEUUUUIIIIOOOOOCNaaaaaeeeeuuuuiiiiooooocn') 
from pessoas;


Talvez haja uma solução mais curta (sei que há em Java), mas não creio que haja uma tão legível.

quarta-feira, 13 de janeiro de 2016

Os problemas médicos da informática

O modelo de medicina no Brasil tem um problema fundamental. Os médicos são remunerados por procedimentos, exames, consultas, etc. Isto é, sua remuneração não aumenta com a saúde dos pacientes. Ela aumenta, isto sim, quanto pior for a saúde das pessoas.

Na informática, isso seria como pagar os programadores por linha de código. Fosse essa a nossa realidade, teríamos legiões de programadores Java e algumas poucas almas iluminadas usando Perl.