
Toda vez que gravamos o Unix Load On (Canal Unix Load On), eu acabo fazendo uma seleção tosca do que vamos falar. Isso sempre me deu uma coceira de resolver. E nada melhor que o bom e velho Python.
Hoje eu coloquei as mangas de fora e fiz funcionar. No bom e velho modo script: rodo num shell, e pego o resultado. Mas cheguei no ponto em que gostaria de que isso estivesse disponível em modo web, pra eu poder mostrar durante o programa diretamente no browser.
O Python fornece o módulo SimpleHTTPServer, que aliás parece que no Python3 virou uma classe de http.serve. Mas tem. Ele mostra o filesystem via interface http a partir de onde você chama o módulo. Não deveria ser complicado fazer o output do meu script ir pra uma interface http. E realmente foi o que fiz.
def start():
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
client_ip, client_port = self.client_address
reqpath = self.path.rstrip()
print(f"request from {client_ip}:{client_port} for {reqpath}")
article = get_final_article()
title = get_title(article)
link = get_link(article)
response = f"""
<h1>Title: <a href="/{link}">{title}
<h2>Link: <a href="/{link}">{link}</a></h2>
"""
content = bytes(response.encode("utf-8"))
self.wfile.write(content)
Se leu com atenção vai ver que a linha do artigo eu pego em "article = get_final_article()". Isso retorna algo como:
* [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)
Dai o restante é sanitizar cada um pra mostrar corretamente.
O script todo pode ser visto aqui: https://github.com/helioloureiro/homemadescripts/blob/master/random_article.py
O resultado no console é algo parecido com isso aqui:
./random_article.py
127.0.0.1 - - [26/Mar/2021 21:31:27] "GET /newarticle? HTTP/1.1" 200 -
request from 127.0.0.1:46060 for /newarticle?
= Articles =
* /helioloureiro/canalunixloadon/blob/master/pautas/2016030.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016040.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016041.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016050.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016051.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016090.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016100.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016110.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016120.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017010.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017020.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017030.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017040.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017060.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017061.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017070.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017080.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017110.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018030.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018050.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018060.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018080.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018100.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2019020.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2019040.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2019070.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2019110.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20200717.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20200807.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201001.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201015.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201029.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201115.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201204.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210121.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210205.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210215.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210312.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210325.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
Latest: /helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
https://raw.githubusercontent.com/helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
Article selected: * [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)
title: Linux developers get ready to wield the secateurs against elderly microprocessors • The Register
article: * [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)

Um dos trabalhos que faço como voluntário é manter alguns serviços "alternativos" na empresa. Todos baseados em software livre.
Dos que são mantidos temos um mediawiki, um encurtador yourls e um etherpad-lite. E esse último foi o que precisei mexer pra transferir pra um servidor novo.
Muitas pessoas gostam do etherpad-lite e o usam, mas devo dizer que por trás é um lixo. Serviço porco. Ele usa uma só tabela no MySQL/MariaDB com dois campos:
mysql> show tables; +-----------------+ | Tables_in_paddb | +-----------------+ | store | +-----------------+ 1 row in set (0.00 sec) mysql> desc store; +-------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+-------+ | key | varchar(100) | NO | PRI | | | | value | longtext | YES | | NULL | | +-------+--------------+------+-----+---------+-------+ 2 rows in set (0.01 sec)
Sério. 2 campos. E só. Um é uma chave toscamente preparada pra ser chave primária e o resto... é valor. Então o uso do DB só cresce, sem chances de uma manutenção decente.
Enquanto o uso do etherpad-lite é um dor nas costelas, o assunto é mais da migração dos dados. Então continuando o assunto, o nosso DB chegou ao incrível valor de 13 GB. Daí como faz a migração? O básico é tirar um dump do DB antigo com mysqldump e carregar usando o comand mysql mesmo.
Algo como isso:
# mysql --host=remote-server.mysql.internal.com --port=1234 --user=sqluser --password=sqlpassword mydb < etherpad-migration-backup.sql
que pra todos efeitos funciona. O único problema foi que depois de passar 15 horas carregando o arquivo...
ERROR 2013 (HY000) at line 19057418: Lost connection to MySQL server during query
Dizem que não tem dor maior que a dor do parto. Tem sim e chama-se carregar um dump de 13 GB por 15 horas e falhar. Assim.
E o que restou fazer. Bom... eu sabia a linha onde estava o arquivo, mas já tinham sido 15 horas num arquivo serial, que faz linha por linha. Então decidi quebrar o dump em vários arquivos menores. Dei um rápido "wc -l" no dump e vi que tinham exatamente 28993313 linhas. Então era possível quebrar em 28 arquivos de 1 milhão de linhas cada. E foi o que fiz.
Assim eu sabia que podia continuar do arquivo 20 em diante. E depois resolvia como fazer com o que faltava.
# split -l 1000000 -d etherpad-migration-backup.sql etherpad-migration-backup.sql. # ls -1 pad-migration-backup.sql.?? etherpad-migration-backup.sql.00 etherpad-migration-backup.sql.01 etherpad-migration-backup.sql.02 etherpad-migration-backup.sql.03 etherpad-migration-backup.sql.04 etherpad-migration-backup.sql.05 etherpad-migration-backup.sql.06 etherpad-migration-backup.sql.07 etherpad-migration-backup.sql.08 etherpad-migration-backup.sql.09 etherpad-migration-backup.sql.10 etherpad-migration-backup.sql.11 etherpad-migration-backup.sql.12 etherpad-migration-backup.sql.13 etherpad-migration-backup.sql.14 etherpad-migration-backup.sql.15 etherpad-migration-backup.sql.16 etherpad-migration-backup.sql.17 etherpad-migration-backup.sql.18 etherpad-migration-backup.sql.19 etherpad-migration-backup.sql.20 etherpad-migration-backup.sql.21 etherpad-migration-backup.sql.22 etherpad-migration-backup.sql.23 etherpad-migration-backup.sql.24 etherpad-migration-backup.sql.25 etherpad-migration-backup.sql.26 etherpad-migration-backup.sql.27 etherpad-migration-backup.sql.28 etherpad-migration-backup.sql.29
Com isso eu tive vários arquivos que eu podia subir em paralelo. E foi o que fiz. O resultado? Não só um mas vários erros depois de algumas horas carregando. Eu queria chorar. No chuveiro. Em posição fetal. Só isso.
O maldito do comando mysql não te permite dar um replay descartando o que já existisse no DB, o que seria uma mão na roda nessas situações. Então fiz isso com python. Mas achei que seria lento demais manter serializado. Então era um bom momento pra testar o asyncio, que usei pouquíssimo até hoje. E valeu muito a pena. Esse é o script final:
#! /usr/bin/python3
import sys
import pymysql.cursors
import asyncio
connection = pymysql.connect(host="remote-server.mysql.internal.com",
port=1234,
user="sqluser",
password="sqlpassword",
db="mydb",
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor)
cursor = connection.cursor()
sema = asyncio.Semaphore(value=10)
async def commit_line(line):
await sema.acquire()
print(line)
try:
cursor.execute(line)
connection.commit()
except:
print("Line (", line[:10],") already inserted")
pass
sema.release()
with open(sys.argv[1]) as sqlfile:
loop = asyncio.get_event_loop()
for line in sqlfile.readlines():
loop.run_until_complete( commit_line(line) )
loop.close()
Não está dos mais polidos, e com senha dentro, mas era uma coisa rápida pra resolver meu problema. E resolveu.
Eu criei uma fila de 10 processos em paralelo pra rodar com: sema = asyncio.Semaphore(value=10)
o controle de acesso ao processo pra rodar é feito com sema.acquire() e sema.release(). Muito fácil. Nem precisei criar um objeto Queue.
Dentro do loop do commit_line() eu sabugue um "enfia essa linha lá ou então continua". Simples assim. E funcionou.
Eu já tinha deixado o tmux aberto com várias janelas, uma pra cada arquivo, então foi só rodar o mesmo em cada uma que falhou.
Levou mais umas 2 ou 3 horas mas carregou tudo.
Foi lindo, não foi?

Hoje chegou um mail pedindo pra testar uma mudança de máquinas que saíram da empresa pra irem habitar o cloud. A tarefa era testar máquina e porta. Algumas máquinas com uma porta somente, outras com várias. E todas TCP.
Pra fazer isso rapidamente eu escrevi um script em python3 que basicamente estabelece uma conexão TCP e mostra OK se conectar ou FAIL se não conseguir. Bem básico, mas resolveu meu problema muito mais rápido que se eu fosse testar máquina por máquina, porta por porta provavelmente com o comando telnet.
#! /usr/bin/python3
import socket
servers = [
"helio.loureiro.eng.br:443",
"helio.loureiro.eng.br:389",
"helio.loureiro.eng.br:8081",
"helio.loureiro.eng.br:80",
"helio.loureiro.eng.br:22"
]
for server in servers:
try:
host, port = server.split(":")
port = int(port)
socket.create_connection((host, port), 3)
print(server, "OK")
except:
print(server, "FAIL")
O resultado:
helio.loureiro.eng.br:443 OK helio.loureiro.eng.br:389 FAIL helio.loureiro.eng.br:8081 FAIL helio.loureiro.eng.br:80 OK helio.loureiro.eng.br:22 OK
Boa diversão!

Escrevi um artigo pro blog "ChurrOps on DevOps" sobre raspresenterpy aṕos uma conversa sobre o assunto no canal do Telegram do grupo "papo de sysadmin".
Eu achava que já tinha escrito sobre isso aqui mesmo, mas fiquei surpreso por não ter encontrado nenhuma referência.
raspresenterpy é um programa de display que alterna links pra ser usado pra mostrar mensagens ou telas de monitoramento como do Jenkins. Fiz até uma apresentação sobre o mesmo numa PyConSE. O texto que escrevi detalha sobre repositório e como usar o mesmo.
Ele encontra-se com suporte ao raspbian Jessie e python-qt4. Testei com python-qt5 e já verifiquei que não funciona. Então deixei no meu backlog pra corrigir e lançar uma versão mais recente.
https://churrops.io/2018/06/08/usando-python-e-raspberrypi-pra-mostrar-varias-telas/


Esses dias estava analisando um código que era parecido com shell script. Sua função era monitorar as interfaces de rede para, no caso de alteração (nova interface surgindo ou desaparecendo), registrar a mudança de objetos no sistema de dados do opensaf (objetos no IMM pra quem conhece).
Mas o código fazia algo parecido com um shell que a todo healthcheck do AMF (framework do opensaf que é muito semelhante ao systemd e roda determinado programa ou script de tempos em tempos) fazia uma busca com o comando "ip link addr list" e comparava com o que estava armazenado no IMM. Algo como:
def healthcheckCallback(self, invocation, compName, healthcheckKey): saAmfResponse(self.check_macs, invocation, eSaAisErrorT.SA_AIS_OK)
def check_macs(self):
macs = []
for line in self.get_link_list().split("\n"):
if not re.search("link/ether", line): continue
# [ 'link/ether', '52:54:00:c3:5d:88', 'brd', 'ff:ff:ff:ff:ff:ff']
mac.append(line.split()[1])
imm_obj = self.get_from_imm()
if imm_obj != macs:
self.update_imm(mac)
def get_link_list(self):
linux_command = "ip link list"
return self.run_shell(linux_command)
Essa é uma forma bastante simplificada pra tentar visualizar como tudo funciona. Eu propositalmente tirei comentários extras e deixei mais limpo apenas para poder comentar aqui.
Como a chamada pra buscar os dados junto ao IMM no OpenSAF tem muitas linhas, eu só deixei um get_from_imm() que retornará um array de mac registrados anteriormente. Se esse valor for diferente do coletado, então é chamado o método update_imm() com os macs que devem estar lá.
Funciona? Sim, funciona. Mas... se não houve nenhuma mudança nas interfaces de rede (como a subida de uma interface de VIP ou mesmo um container em docker), por quê eu preciso rodar o get_link_list()?
Entendeu qual foi meu ponto?
O código em si consiste em rodar o monitoramente separado numa thread. Toda vez que o código detecta uma mudança (na verdade o kernel sinaliza isso), ele altera uma variável que o programa lê durante o healthcheck. Algo como:
def check_macs(self):
if self.network_changed is False: return
Assim bem simples. Teve mudança? network_changed vira um True.
Linux tem mecanismos pra detectar uma mudanças na interfaces de rede. Por quê não usar? E foi o que fiz.
Criei um método chamado monitor_link() que é iniciado junto com programa no método initialize(), que é parte de como o AMF faz as chamadas de callback:
self.thread = threading.Thread(target=self.monitor_link, args=())
self.thread.start()
E como funciona o monitor_link()? Aqui tenho de pedir desculpas antecipadamente que enquanto o código utiliza menos CPU e memória que chamar um shell script, o tamanho e complexidade é bastante grande. No fim troquei 2 linhas de código por umas 35 linhas. Na verdade eu praticamente escrevi o código por trás do "ip link". Mas o resultado ficou independente desse comando e mesmo de utilizar um shell externo pra buscar o resultado.
A primeira coisa é criar um socket do tipo AF_NETLINK. Em seguida fazer um bind() num ID aleatório e monitorar com RTMGRP_LINK.
def monitor_link(self):
# Create the netlink socket and bind to RTMGRP_LINK,
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
s.bind((os.getpid(), RTMGRP_LINK))
pra gerar o código aleatório que é um inteiro, usei os.getpid() pra usar o PID do próprio programa.
Em seguida é iniciado um loop com select() em cima do descritor do socket pra leitura. Quando aparecer algum dado, daí sim a informação é lida.
rlist, wlist, xlist = select.select([s.fileno()], [], [], 1)
O que vem a seguir são quebras da sequência de bits até chegar no ponto é possível ver o tipo de mensagem que chegou do select(). Se o tipo de mensagem for NOOP de algo nulo, apenas continue monitorando no select(). Se vier algum ERROR, pare o programa. Se vier mensagem e não for do tipo NEWLINK pra um link novo ou mudança de MAC, também continue aguardando no select().
if msg_type == NLMSG_NOOP: continue elif msg_type == NLMSG_ERROR: break elif msg_type != RTM_NEWLINK: continue
Por fim uma iteração nos dados pra buscar o tipo. Se o dado for do tipo IFLA_IFNAME, que é uma nova interface ou mudança de nome, ou IFLA_ADDRESS, que é MAC e endereço IP, muda a flag de network_changed pra True.
rta_type == IFLA_IFNAME or rta_type == IFLA_ADDRESS:
E é isso. O código completo segue abaixo.
def monitor_link(self):
# Create the netlink socket and bind to RTMGRP_LINK,
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
s.bind((os.getpid(), RTMGRP_LINK))
while self.terminating is False:
rlist, wlist, xlist = select.select([s.fileno()], [], [], 1)
if self.network_changed is True: continue
if self.terminating is True: return
try:
data = os.read(rlist[0], 65535)
except:
continue
msg_len, msg_type, flags, seq, pid = struct.unpack("=LHHLL", data[:16])
if msg_type == NLMSG_NOOP: continue
elif msg_type == NLMSG_ERROR: break
elif msg_type != RTM_NEWLINK: continue
data = data[16:]
family, _, if_type, index, flags, change = struct.unpack("=BBHiII", data[:16])
remaining = msg_len - 32
data = data[16:]
while remaining:
rta_len, rta_type = struct.unpack("=HH", data[:4])
if rta_len < 4: break
rta_data = data[4:rta_len]
increment = (rta_len + 4 - 1) & ~(4 - 1)
data = data[increment:]
remaining -= increment
if rta_type == IFLA_IFNAME or rta_type == IFLA_ADDRESS:
self.network_changed = True
Encontrei essa implementação no Stack Overflow buscando informação do código em C. Foi uma grande ajuda e deixou meu programa muito mais coerente com o que eu realmente queria.
Ficou muito maior? Ficou. Mas também ficou muito mais 1337 :)
Mais:
[1] Exemplo de uso de python com AMF no OpenSAF: https://sourceforge.net/p/opensaf/staging/ci/default/tree/python/samples/amf_demo
[2] Projeto OpenSAF: https://sourceforge.net/projects/opensaf/
[3] Implementação original desse código: https://stackoverflow.com/questions/44299342/netlink-interface-listener-in-python
script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js">