Eu tenho um laptop pessoal que é um Thinkpad T480. Escrevi sobre o mesmo aqui: entrei pra moda do laptop refurbished. Como não estou usando ele pra muita coisa e tenho também um Thinkpad pro trabalho, deixo o meu pra rodar o último Ubuntu.
E estava com o oneiric, 24.10, quando tentei fazer upgrade pro plucky, 25.04. E tive um belo dum crash no zfs.
------------[ cut here ]------------
WARNING: CPU: 0 PID: 227 at drivers/usb/typec/ucsi/ucsi.c:1390 ucsi_reset_ppm+0x1ad/0x1c0 [typec_ucsi]
Modules linked in: zfs(PO) spl(O) dm_crypt hid_multitouch hid_generic cdc_ncm cdc_ether usbnet uas mii usbhid hid usb_storage crct10dif_pclmul crc32_pclmul polyval_clmulni polyval_generic nvme ghash_clmulni_intel snd sha256_ssse3 soundcore sha1_ssse3 nvme_core e1000e video thunderbolt ucsi_acpi psmouse nvme_auth xhci_pci typec_ucsi typec xhci_pci_renesas sparse_keymap platform_profile wmi aesni_intel crypto_simd cryptd
CPU: 0 UID: 0 PID: 227 Comm: kworker/0:2 Tainted: P O 6.11.0-25-generic #25-Ubuntu
Tainted: [P]=PROPRIETARY_MODULE, [O]=OOT_MODULE
Hardware name: LENOVO 20L6S4G700/20L6S4G700, BIOS N24ET76W (1.51 ) 02/27/2024
Workqueue: events_long ucsi_init_work [typec_ucsi]
RIP: 0010:ucsi_reset_ppm+0x1ad/0x1c0 [typec_ucsi]
Code: ff 8b 55 bc 81 e2 00 00 00 08 0f 85 33 ff ff ff 4c 89 75 c8 48 8b 05 72 9d 4a cb 49 39 c5 79 94 b8 92 ff ff ff e9 19 ff ff ff <0f> 0b e9 57 ff ff ff e8 17 1b 17 ca 0f 1f 80 00 00 00 00 90 90 90
RSP: 0018:ffffba53c03a3d80 EFLAGS: 00010206
RAX: 0000000008000000 RBX: ffff9d0102192800 RCX: 0000000000000000
RDX: 00000000fffb83c0 RSI: 0000000000000000 RDI: 0000000000000000
RBP: ffffba53c03a3dd0 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: ffffba53c03a3d8c
R13: 00000000fffb83be R14: ffff9d0101a4fc00 R15: ffff9d01021928c0
FS: 0000000000000000(0000) GS:ffff9d0666200000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00005799b59ce675 CR3: 00000003afe3e004 CR4: 00000000003706f0
DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
Call Trace:
<TASK>
? show_trace_log_lvl+0x1be/0x310
? show_trace_log_lvl+0x1be/0x310
? ucsi_init+0x32/0x310 [typec_ucsi]
? show_regs.part.0+0x22/0x30
? show_regs.cold+0x8/0x10
? ucsi_reset_ppm+0x1ad/0x1c0 [typec_ucsi]
? __warn.cold+0xa7/0x101
? ucsi_reset_ppm+0x1ad/0x1c0 [typec_ucsi]
? report_bug+0x114/0x160
? handle_bug+0x6e/0xb0
? exc_invalid_op+0x18/0x80
? asm_exc_invalid_op+0x1b/0x20
? ucsi_reset_ppm+0x1ad/0x1c0 [typec_ucsi]
ucsi_init+0x32/0x310 [typec_ucsi]
ucsi_init_work+0x18/0x90 [typec_ucsi]
process_one_work+0x174/0x350
worker_thread+0x31a/0x450
? _raw_spin_lock_irqsave+0xe/0x20
? __pfx_worker_thread+0x10/0x10
kthread+0xe1/0x110
? __pfx_kthread+0x10/0x10
ret_from_fork+0x44/0x70
? __pfx_kthread+0x10/0x10
ret_from_fork_asm+0x1a/0x30
</TASK>
---[ endtrace 0000000000000000 ]---
WARNING: CPU: 0 PID: 978 at /build/linux-Ajk80v/linux-6.11.0/debian/build/build-generic/____________________________________________________________________________dkms/build/zfs/2.2.6/build/module/zfs/zfs_log.c:817 zfs_log_setsaxattr+0x140/0x150 [zfs]
Modules linked in: msr(+) parport_pc ppdev lp parport efi_pstore nfnetlink dmi_sysfs ip_tables x_tables autofs4 typec_displayport zfs(PO) spl(O) dm_crypt hid_multitouch hid_generic cdc_ncm cdc_ether usbnet uas mii usbhid hid usb_storage crct10dif_pclmul crc32_pclmul polyval_clmulni polyval_generic nvme ghash_clmulni_intel snd sha256_ssse3 soundcore sha1_ssse3 nvme_core e1000e video thunderbolt ucsi_acpi psmouse nvme_auth xhci_pci typec_ucsi typec xhci_pci_renesas sparse_keymap platform_profile wmi aesni_intel crypto_simd cryptd
CPU: 0 UID: 0 PID: 978 Comm: systemd-random- Tainted: P W O 6.11.0-25-generic #25-Ubuntu
Tainted: [P]=PROPRIETARY_MODULE, [W]=WARN, [O]=OOT_MODULE
Hardware name: LENOVO 20L6S4G700/20L6S4G700, BIOS N24ET76W (1.51 ) 02/27/2024
RIP: 0010:zfs_log_setsaxattr+0x140/0x150 [zfs]
Code: ff ff ff 31 c9 48 c7 c2 c0 94 e5 c0 4c 89 f6 4c 89 55 c0 48 c7 c7 68 8e e5 c0 4c 89 4d d0 c6 05 4a 50 13 00 01 e8 a0 c3 a9 c8 <0f> 0b 4c 8b 55 c0 4c 8b 4d d0 e9 30 ff ff ff 90 90 90 90 90 90 90
RSP: 0018:ffffba53c192f748 EFLAGS: 00010246
RAX: 0000000000000000 RBX: ffff9d0107f7bdb0 RCX: 0000000000000000
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
RBP: ffffba53c192f790 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: ffff9d011613e800
R13: ffff9d010b3499c0 R14: 000000000000001c R15: 0000000000000000
FS: 00007a57caef8980(0000) GS:ffff9d0666200000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007a57cbf071e0 CR3: 000000010a016003 CR4: 00000000003706f0
DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
Call Trace:
<TASK>
? show_trace_log_lvl+0x1be/0x310
? show_trace_log_lvl+0x1be/0x310
? zfs_sa_set_xattr+0x34a/0x3b0 [zfs]
? show_regs.part.0+0x22/0x30
? show_regs.cold+0x8/0x10
? zfs_log_setsaxattr+0x140/0x150 [zfs]
? __warn.cold+0xa7/0x101
? zfs_log_setsaxattr+0x140/0x150 [zfs]
? report_bug+0x114/0x160
? handle_bug+0x6e/0xb0
? exc_invalid_op+0x18/0x80
? asm_exc_invalid_op+0x1b/0x20
? zfs_log_setsaxattr+0x140/0x150 [zfs]
? zfs_log_setsaxattr+0x140/0x150 [zfs]
zfs_sa_set_xattr+0x34a/0x3b0 [zfs]
zpl_xattr_set_sa+0x102/0x200 [zfs]
zpl_xattr_set+0x21c/0x290 [zfs]
__zpl_xattr_user_set+0x128/0x170 [zfs]
zpl_xattr_user_set+0x22/0x40 [zfs]
__vfs_removexattr+0x81/0xd0
__vfs_removexattr_locked+0xe5/0x1a0
? touch_atime+0xbe/0x120
vfs_removexattr+0x59/0x110
__do_sys_fremovexattr+0x130/0x1c0
__x64_sys_fremovexattr+0x15/0x20
x64_sys_call+0x1fc7/0x22b0
do_syscall_64+0x7e/0x170
? filemap_map_pages+0x34f/0x570
? xa_load+0x73/0xb0
? do_read_fault+0xfd/0x200
? do_fault+0x183/0x210
? generic_file_llseek+0x24/0x40
? zpl_llseek+0x32/0xd0 [zfs]
? ksys_lseek+0x7d/0xd0
? syscall_exit_to_user_mode+0x4e/0x250
? do_syscall_64+0x8a/0x170
? __count_memcg_events+0x86/0x160
? count_memcg_events.constprop.0+0x2a/0x50
? handle_mm_fault+0x1bb/0x2d0
? do_user_addr_fault+0x5e9/0x7e0
? irqentry_exit_to_user_mode+0x43/0x250
? irqentry_exit+0x43/0x50
? exc_page_fault+0x96/0x1c0
entry_SYSCALL_64_after_hwframe+0x76/0x7e
RIP: 0033:0x7a57cba5d4eb
Code: 73 01 c3 48 8b 0d 0d 79 0e 00 f7 d8 64 89 01 48 83 c8 ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa b8 c7 00 00 00 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d dd 78 0e 00 f7 d8 64 89 01 48
RSP: 002b:00007ffe118d0498 EFLAGS: 00000246 ORIG_RAX: 00000000000000c7
RAX: ffffffffffffffda RBX: 0000000000000004 RCX: 00007a57cba5d4eb
RDX: 000000000000001a RSI: 00006015c386c08b RDI: 0000000000000005
RBP: 00007ffe118d05d0 R08: 00007a57cbb45b20 R09: 00000000000000c0
R10: 0000601603619fc0 R11: 0000000000000246 R12: 0000000000000005
R13: 0000000000000001 R14: 0000000000000000 R15: 0000000000000001
</TASK>
E ficava nisso.
Tinha de mandar um zfs rollback
nos volumes pra conseguir voltar a usar.
E tentar o upgrade novamente.
Depois de muito tentar, resolvi abrir um bug report no launchpad. Meu bug foi marcado como duplicado e passei então a interagir no bug onde o problema foi reportado primeiramente.
tl;dr: o bug era do zfs no kernel padrão que o plucky instala. A correção exige upgrade tanto do zfs quanto do kernel antes de ir pro upgrade do plucky.
Da primeira vez eu errei esse upgrade. E precisei recuperar o zfs pra voltar o snapshot.
E mais um problema já que não existe um procedimento bem descritivo de como fazer isso. Ou tem?
Sim tem. E mais de um.
Tive de recuperar algumas vezes o sistema. Então fiquei meio que craque em fazer isso. O esquema está abaixo:
root@ubuntu:~# lsblk -f
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS
loop0 squashfs 4.0 0 100% /rofs
loop1 squashfs 4.0
loop2 squashfs 4.0
loop3 squashfs 4.0 0 100% /snap/bare/5
loop4 squashfs 4.0 0 100% /snap/core22/1748
loop5 squashfs 4.0 0 100% /snap/firefox/5751
loop6 squashfs 4.0 0 100% /snap/firmware-updater/167
loop7 squashfs 4.0 0 100% /snap/gnome-42-2204/202
loop8 squashfs 4.0 0 100% /snap/gtk-common-themes/1535
loop9 squashfs 4.0 0 100% /snap/snap-store/1248
loop10 squashfs 4.0 0 100% /snap/thunderbird/644
loop11 squashfs 4.0 0 100% /snap/ubuntu-desktop-bootstrap/315
loop12 squashfs 4.0 0 100% /snap/snapd-desktop-integration/253
loop13 squashfs 4.0 0 100% /snap/snapd/23545
sda iso9660 Joliet Extension Ubuntu 24.04.2 LTS amd64 2025-02-15-09-15-26-00
├─sda1 iso9660 Joliet Extension Ubuntu 24.04.2 LTS amd64 2025-02-15-09-15-26-00 0 100% /cdrom
├─sda2 vfat FAT12 ESP B5A5-8010
├─sda3
└─sda4 ext4 1.0 writable 5729a291-83ad-4b15-91b1-09a17bfc9504 1.3G 4% /var/crash
/var/log
sdb
nvme0n1
├─nvme0n1p1 vfat FAT32 C399-15AF
├─nvme0n1p2 zfs_member 5000 bpool 4626876014803904226
├─nvme0n1p3
└─nvme0n1p4 zfs_member 5000 rpool 15334588309526604034
root@ubuntu:~# zpool import -f rpool
root@ubuntu:~# zpool list
NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
rpool 936G 365G 571G - - 6% 39% 1.00x ONLINE -
root@ubuntu:~# cryptsetup open /dev/zvol/rpool/keystore rpool-keystore
Enter passphrase for /dev/zvol/rpool/keystore:
root@ubuntu:~# mkdir /mnt-keystore
root@ubuntu:~# mount /dev/mapper/rpool-keystore /mnt-keystore
root@ubuntu:~# ls /mnt-keystore
lost+found system.key
root@ubuntu:~# cat /mnt-keystore/system.key | zfs load-key -L prompt rpool
root@ubuntu:~# umount /mnt-keystore
root@ubuntu:~# cryptsetup close rpool-keystore
root@ubuntu:~# zfs list
NAME USED AVAIL REFER MOUNTPOINT
rpool 365G 542G 192K /
rpool/ROOT 106G 542G 192K none
rpool/ROOT/ubuntu_ni6nkv 106G 542G 11.5G /mnt
rpool/ROOT/ubuntu_ni6nkv/srv 352K 542G 192K /mnt/srv
rpool/ROOT/ubuntu_ni6nkv/usr 7.96M 542G 192K /mnt/usr
rpool/ROOT/ubuntu_ni6nkv/usr/local 7.77M 542G 6.02M /mnt/usr/local
rpool/ROOT/ubuntu_ni6nkv/var 64.4G 542G 192K /mnt/var
rpool/ROOT/ubuntu_ni6nkv/var/games 272K 542G 192K /mnt/var/games
rpool/ROOT/ubuntu_ni6nkv/var/lib 60.0G 542G 24.2G /mnt/var/lib
rpool/ROOT/ubuntu_ni6nkv/var/lib/AccountsService 1.07M 542G 212K /mnt/var/lib/AccountsService
rpool/ROOT/ubuntu_ni6nkv/var/lib/NetworkManager 7.59M 542G 580K /mnt/var/lib/NetworkManager
rpool/ROOT/ubuntu_ni6nkv/var/lib/apt 388M 542G 103M /mnt/var/lib/apt
rpool/ROOT/ubuntu_ni6nkv/var/lib/dpkg 1.14G 542G 169M /mnt/var/lib/dpkg
rpool/ROOT/ubuntu_ni6nkv/var/log 261M 542G 92.7M /mnt/var/log
rpool/ROOT/ubuntu_ni6nkv/var/mail 272K 542G 192K /mnt/var/mail
rpool/ROOT/ubuntu_ni6nkv/var/snap 4.11G 542G 4.03G /mnt/var/snap
rpool/ROOT/ubuntu_ni6nkv/var/spool 10.6M 542G 468K /mnt/var/spool
rpool/ROOT/ubuntu_ni6nkv/var/www 55.3M 542G 55.1M /mnt/var/www
rpool/USERDATA 258G 542G 192K none
rpool/USERDATA/home_39e1h7 258G 542G 242G /home
rpool/USERDATA/root_39e1h7 58.8M 542G 28.4M /root
rpool/keystore 39.8M 542G 16.5M -
rpool/var 739M 542G 192K /var
rpool/var/lib 739M 542G 192K /var/lib
rpool/var/lib/docker 738M 542G 729M /var/lib/docker
root@ubuntu:~# zfs set mountpoint=/mnt rpool/ROOT/ubuntu_ni6nkv
root@ubuntu:~# zfs mount rpool/ROOT/ubuntu_ni6nkv
root@ubuntu:~# ls /mnt
bin boot cdrom dev etc home lib lib32 lib64 media mnt opt proc root run sbin snap srv sys tmp usr var
root@ubuntu:~# zpool import -N -R /mnt bpool
root@ubuntu:~# zfs mount bpool/BOOT/ubuntu_ni6nkv
root@ubuntu:~# ls /mnt/boot/
System.map-6.11.0-21-generic config-6.11.0-21-generic efi initrd.img-6.11.0-21-generic initrd.img.old memtest86+x64.bin vmlinuz-6.11.0-21-generic vmlinuz.old
System.map-6.11.0-24-generic config-6.11.0-24-generic grub initrd.img-6.11.0-24-generic memtest86+ia32.bin memtest86+x64.efi vmlinuz-6.11.0-24-generic
System.map-6.14.0-15-generic config-6.14.0-15-generic initrd.img initrd.img-6.14.0-15-generic memtest86+ia32.efi vmlinuz vmlinuz-6.14.0-15-generic
root@ubuntu:~# mount /dev/nvme0n1p1 /mnt/boot/efi/
root@ubuntu:~# ls /mnt/boot/efi/
EFI
root@ubuntu:~# for i in proc dev sys dev/pts; do mount -v --bind /$i /mnt/$i; done
mount: /proc bound on /mnt/proc.
mount: /dev bound on /mnt/dev.
mount: /sys bound on /mnt/sys.
mount: /dev/pts bound on /mnt/dev/pts.
root@ubuntu:~# zfs set mountpoint=/ rpool/ROOT/ubuntu_ni6nkv
Broadcast message from systemd-journald@ubuntu (Sat 2025-04-19 15:35:17 UTC):
systemd[1]: Caught , from our own process.
root@ubuntu:~# zfs list
NAME USED AVAIL REFER MOUNTPOINT
bpool 838M 953M 96K /mnt/boot
bpool/BOOT 833M 953M 96K none
bpool/BOOT/ubuntu_ni6nkv 833M 953M 295M /mnt/boot
rpool 365G 542G 192K /
rpool/ROOT 106G 542G 192K none
rpool/ROOT/ubuntu_ni6nkv 106G 542G 11.5G /
rpool/ROOT/ubuntu_ni6nkv/srv 352K 542G 192K /srv
rpool/ROOT/ubuntu_ni6nkv/usr 7.96M 542G 192K /usr
rpool/ROOT/ubuntu_ni6nkv/usr/local 7.77M 542G 6.02M /usr/local
rpool/ROOT/ubuntu_ni6nkv/var 64.4G 542G 192K /var
rpool/ROOT/ubuntu_ni6nkv/var/games 272K 542G 192K /var/games
rpool/ROOT/ubuntu_ni6nkv/var/lib 60.0G 542G 24.2G /var/lib
rpool/ROOT/ubuntu_ni6nkv/var/lib/AccountsService 1.07M 542G 212K /var/lib/AccountsService
rpool/ROOT/ubuntu_ni6nkv/var/lib/NetworkManager 7.59M 542G 580K /var/lib/NetworkManager
rpool/ROOT/ubuntu_ni6nkv/var/lib/apt 388M 542G 103M /var/lib/apt
rpool/ROOT/ubuntu_ni6nkv/var/lib/dpkg 1.14G 542G 169M /var/lib/dpkg
rpool/ROOT/ubuntu_ni6nkv/var/log 261M 542G 92.7M /var/log
rpool/ROOT/ubuntu_ni6nkv/var/mail 272K 542G 192K /var/mail
rpool/ROOT/ubuntu_ni6nkv/var/snap 4.11G 542G 4.03G /var/snap
rpool/ROOT/ubuntu_ni6nkv/var/spool 10.6M 542G 468K /var/spool
rpool/ROOT/ubuntu_ni6nkv/var/www 55.3M 542G 55.1M /var/www
rpool/USERDATA 258G 542G 192K none
rpool/USERDATA/home_39e1h7 258G 542G 242G /home
rpool/USERDATA/root_39e1h7 58.8M 542G 28.4M /root
rpool/keystore 39.8M 542G 16.5M -
rpool/var 739M 542G 192K /var
rpool/var/lib 739M 542G 192K /var/lib
rpool/var/lib/docker 738M 542G 729M /var/lib/docker
root@ubuntu:~# zfs set mountpoint=/boot bpool
root@ubuntu:~# zfs list
NAME USED AVAIL REFER MOUNTPOINT
bpool 838M 953M 96K /mnt/boot
bpool/BOOT 833M 953M 96K none
bpool/BOOT/ubuntu_ni6nkv 833M 953M 295M /mnt/boot
rpool 365G 542G 192K /
rpool/ROOT 106G 542G 192K none
rpool/ROOT/ubuntu_ni6nkv 106G 542G 11.5G /
rpool/ROOT/ubuntu_ni6nkv/srv 352K 542G 192K /srv
rpool/ROOT/ubuntu_ni6nkv/usr 7.96M 542G 192K /usr
rpool/ROOT/ubuntu_ni6nkv/usr/local 7.77M 542G 6.02M /usr/local
rpool/ROOT/ubuntu_ni6nkv/var 64.4G 542G 192K /var
rpool/ROOT/ubuntu_ni6nkv/var/games 272K 542G 192K /var/games
rpool/ROOT/ubuntu_ni6nkv/var/lib 60.0G 542G 24.2G /var/lib
rpool/ROOT/ubuntu_ni6nkv/var/lib/AccountsService 1.07M 542G 212K /var/lib/AccountsService
rpool/ROOT/ubuntu_ni6nkv/var/lib/NetworkManager 7.59M 542G 580K /var/lib/NetworkManager
rpool/ROOT/ubuntu_ni6nkv/var/lib/apt 388M 542G 103M /var/lib/apt
rpool/ROOT/ubuntu_ni6nkv/var/lib/dpkg 1.14G 542G 169M /var/lib/dpkg
rpool/ROOT/ubuntu_ni6nkv/var/log 261M 542G 92.7M /var/log
rpool/ROOT/ubuntu_ni6nkv/var/mail 272K 542G 192K /var/mail
rpool/ROOT/ubuntu_ni6nkv/var/snap 4.11G 542G 4.03G /var/snap
rpool/ROOT/ubuntu_ni6nkv/var/spool 10.6M 542G 468K /var/spool
rpool/ROOT/ubuntu_ni6nkv/var/www 55.3M 542G 55.1M /var/www
rpool/USERDATA 258G 542G 192K none
rpool/USERDATA/home_39e1h7 258G 542G 242G /home
rpool/USERDATA/root_39e1h7 58.8M 542G 28.4M /root
rpool/keystore 39.8M 542G 16.5M -
rpool/var 739M 542G 192K /var
rpool/var/lib 739M 542G 192K /var/lib
rpool/var/lib/docker 738M 542G 729M /var/lib/docker
No fim deu certo e consegui fazer upgrade pro plucky. Mas o problema ainda existe. Não sei se será um problema quando chegar a época de upgrade do 24.04.
Espero que não.
Esse é um dos vídeos mais hilários que vi aqui na Suécia. Pra encurtar a história, aqui tinha um carrinho de cachorros quentes que servia uma salsicha super apimentada. Era chamado de harakiri, do termo japonês pra suicídio, e tornou-se um trend aqui por um tempo.
O vídeo funciona com legendas em inglês. Então é possível assistir e gargalhar gostoso com a reportagem. Era mais apimentado que spray de pimenta que a polícia usa.
E eu já tentei essa iguaria?
Não.
E não vou nunca mais nem tentar porque o dono do carrinho parou de servir. Não que isso mudasse minha opinião sobre não tentar. Mas ele achou que estava chamando muita a atenção e tirando sua privacidade. Vendeu tudo e foi vender hambúrgueres em Malmö.
Update: eu vi que o vocêtubo não está funcionando corretamente nos telefones. Então aqui embaixo está o link pra vídeo.
Esses dias eu vi um post no Mastodon sobre bloquear robôs que acessam o servidor web.
Perguntei pro autor do post como foi que ele conseguiu isso, mas fui solenemente ignorado. Coisas da Internet.
Então resolvi dar uma olhada nos logs do servidor, esse que hospeda esse mesmo site. E fiz um programa em perl pra isso. Pra matar as saudades. E tirar a ferrugem.
E esse foi o programa:
#! /usr/bin/env perl
use IO::Zlib;
my $LOGDIR = "/var/log/apache2";
opendir(DIR, $LOGDIR) or die "Impossible to read from directory: $!\n";
%ip_addrs;
%bot_agent;
@gzip_files;
foreach my $filename (readdir DIR) {
next if $filename !~ /access/;
# skip gz right now
if ($filename =~ /\.gz/) {
push(@gzip_files, ($LOGDIR."/".$filename));
next;
}
print($LOGDIR."/".$filename."\n");
open(FD, $LOGDIR."/".$filename) or die "Impossible to read file: $!\n";
foreach my $line () {
next if $line !~ /bot/;
parse_log_line($line);
}
}
print("result:\n");
for my $filename (@gzip_files) {
print("$filename\n");
my $fh = new IO::Zlib;
$fh->open($filename, "rb") or die "impossible to read gzip file: $!\n";
while ( my $line = <$fh>) {
next if $line !~ /bot/;
parse_log_line($line);
}
}
foreach $bot (sort {$bot_agent{$b}<=>$bot_agent{$a} } keys %bot_agent) {
print("$bot => $bot_agent{$bot}\n");
}
sub parse_log_line() {
my $line = $_[0];
our %ip_addrs;
our %bot_agent;
@params = split(/ /, $line);
my $ip = $params[0];
$ip_addrs{$ip}++;
$line =~ s/.*]//;
$line =~ s/\"$//;
$line =~s/.*\"//;
chomp($line);
if ($line =~ m/bot/) {
$bot_agent{$line}++;
}
}
E o resultado em formato de tabela:
User-Agent do robô | Quantidade de acessos |
---|---|
Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/) | 30130 |
Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html) | 22203 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; | 16981 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/116.0.1938.76 Safari/537.36 | 15979 |
Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/) | 12112 |
Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://webmaster.petalsearch.com/site/petalbot) | 10744 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Amazonbot/0.1; +https://developer.amazon.com/support/amazonbot) Chrome/119.0.6045.214 Safari/537.36 | 9704 |
Mozilla/5.0 (compatible; DotBot/1.2; +https://opensiteexplorer.org/dotbot; | 7434 |
Mozilla/5.0 (compatible; AwarioBot/1.0; +https://awario.com/bots.html) | 5042 |
Mozilla/5.0 (compatible; DataForSeoBot/1.0; +https://dataforseo.com/dataforseo-bot) | 3104 |
Linguee Bot (http://www.linguee.com/bot; | 1957 |
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot) | 1450 |
Mozilla/5.0 (compatible; archive.org_bot +http://archive.org/details/archive.org_bot) Zeno/5741de8 warc/v0.8.85 | 751 |
Mozilla/5.0 (compatible; SemrushBot-BA; +http://www.semrush.com/bot.html) | 651 |
Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots) | 567 |
Mozilla/5.0 (compatible; MJ12bot/v2.0.2; http://mj12bot.com/) | 538 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.183 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 531 |
Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 411 |
Blogtrottr/2.1 (+https://blogtrottr.com/robot) | 340 |
Googlebot-Image/1.0 | 315 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/100.0.4896.127 Safari/537.36 | 300 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +https://openai.com/gptbot) | 261 |
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36; compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot | 240 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.7151.119 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 207 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/136.0.0.0 Safari/537.36 | 191 |
ZoominfoBot (zoominfobot at zoominfo dot com) | 164 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot | 150 |
Mozilla/5.0 (compatible; SeznamBot/4.0; +https://o-seznam.cz/napoveda/vyhledavani/en/seznambot-crawler/) | 124 |
Mozilla/5.0 (compatible; MojeekBot/0.11; +https://www.mojeek.com/bot.html) | 107 |
Mozilla/5.0 (compatible; archive.org_bot +http://archive.org/details/archive.org_bot) Zeno/a7797cb warc/v0.8.78 | 89 |
DuckDuckBot/1.1; (+http://duckduckgo.com/duckduckbot.html) | 82 |
Mozilla/5.0 (compatible;PetalBot;+https://webmaster.petalsearch.com/site/petalbot) | 80 |
Mozilla/5.0 (compatible; YandexImages/3.0; +http://yandex.com/bots) | 70 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot) | 62 |
Mozilla/5.0 (compatible; YaK/1.0; http://linkfluence.com/; | 61 |
Mozilla/5.0 (compatible; coccocbot-image/1.0; +http://help.coccoc.com/searchengine) | 56 |
Mozilla/5.0 (compatible; wpbot/1.3; +https://forms.gle/ajBaxygz9jSR8p8G9) | 41 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/137.0.7151.119 Safari/537.36 | 37 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.168 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 29 |
Twitterbot/1.0 | 29 |
AdsBot-Google (+http://www.google.com/adsbot.html) | 22 |
Mozilla/5.0 (compatible; intelx.io_bot +https://intelx.io) | 21 |
BufferLinkPreviewBot/1.0 (+https://scraper.buffer.com/about/bots/link-preview-bot) | 19 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 14 |
Mozilla/5.0 (compatible; Thinkbot/0.5.8; +In_the_test_phase,_if_the_Thinkbot_brings_you_trouble,_please_block_its_IP_address._Thank_you.) | 13 |
Mozilla/5.0 (compatible; MJ12bot/v2.0.4; http://mj12bot.com/) | 13 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.183 Mobile Safari/537.36 (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html) | 9 |
Mozilla/4.0 (compatible; fluid/0.0; +http://www.leak.info/bot.html) | 8 |
Mozilla/5.0 (Windows NT 10.0; Win64; x64; trendictionbot0.5.0; trendiction search; http://www.trendiction.de/bot; please let us know of any problems; web at trendiction.com) Gecko/20100101 Firefox/125.0 | 7 |
Googlebot/2.1 (+http://www.google.com/bot.html) | 5 |
Mozilla/5.0 (compatible; SurdotlyBot/1.0; +http://sur.ly/bot.html) | 5 |
Mozilla/5.0 (compatible; Website-info.net-Robot; https://website-info.net/robot) | 4 |
Mozilla/5.0 (compatible; coccocbot-web/1.0; +http://help.coccoc.com/searchengine) | 4 |
Pandalytics/2.0 (https://domainsbot.com/pandalytics/) | 4 |
Mozilla/5.0 (compatible; IbouBot/1.0; | 4 |
DomainStatsBot/1.0 (https://domainstats.com/pages/our-bot) | 4 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/W.X.Y.Z Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 3 |
Mozilla/5.0 (compatible; SeekportBot; +https://bot.seekport.com) | 3 |
serpstatbot/2.1 (advanced backlink tracking bot; https://serpstatbot.com/; | 3 |
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0 | 3 |
yacybot (-global; amd64 Linux 5.15.161; java 11.0.26-internal; America/en) http://yacy.net/bot.html | 2 |
Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/120.0.6099.199 Safari/537.36 | 2 |
Mozilla/5.0 (compatible; Qwantbot/1.0_4396629; +https://help.qwant.com/bot/) | 2 |
DomCopBot (https://www.domcop.com/bot) | 2 |
Mozilla/5.0 (compatible; YandexFavicons/1.0; +http://yandex.com/bots) | 2 |
yacybot (/global; amd64 Linux 5.15.0-140-generic; java 11.0.27; America/en) http://yacy.net/bot.html | 2 |
Synapse (bot; +https://github.com/matrix-org/synapse) | 2 |
Mozilla/5.0 (compatible; archive.org_bot; Wayback Machine Live Record; +http://archive.org/details/archive.org_bot) | 1 |
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) | 1 |
Mozilla/5.0 (compatible; Qwantbot/1.0; +https://help.qwant.com/bot/) | 1 |
Facebot | 1 |
Mozilla/5.0 (compatible; GetHPinfo.com-Bot/0.1; +http://www.gethpinfo.com/bot/ | 1 |
Slack-ImgProxy (+https://api.slack.com/robots) | 1 |
Googlebot-Video/1.0 | 1 |
yacybot (/global; amd64 Linux 6.12.38+deb13-amd64; java 21.0.8; Europe/fr) http://yacy.net/bot.html | 1 |
Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; SMTBot/1.0; +http://www.similartech.com/smtbot) | 1 |
Mastodon/4.5.0-alpha.1+chuckya (http.rb/5.3.1; +https://lab.wheelsbot.dev/) | 1 |
O resultado é texto. Eu só formatei pra ficar mais fácil de visualizar aqui (usando "sd" pra isso). E está hardcoded pra buscar os logs do apache2 em sistemas debian alike.
Dos resultados, confesso que fiquei surpreso. Realmente bastante tráfego vindo de robôs. E vários que eu nunca ouvi falar.
Sobre bloquear ou não, eu por enquanto não mexi em nada e os robôs continuam acessando tudo. Mesmo porque eu não fiz nada qualitativo, pra saber se estão recebendo 200 (ok) ou alguma outra coisa como 404 (not found - não encontrad).
Mas caso eu decida pelo bloqueio, achei um projeto bem interessante no GitHub que já faz a curadoria de robôs "bons" e "ruins".
Eu tenho rodado um tipo de sistema que faz DynDNS pra minhas máquinas que estão atrás de um serviço de DHCP como meu desktop que fica aqui em minha mesa, em casa.
O princípio é que essa máquina acessa uma certa URL a cada 5 minutos. Então a cada 5 minutos eu olho pro log do servidor web que roda nessa porta e pego os dados. Pego o IPv4 e o IPv6. Na verdade pego o que vier e vejo se é IPv4 ou IPv6. Depois olho no mapa de DNS se o valor mudou. Se mudou, altero o serial do map e mando um restart no serviço.
A lógica é simples. Mas eu fiz isso uns 15 anos atrás. Pra python2.Alguma-Coisa. Portei pra python3 no melhor estilo #XGH. O resultado? Problemas aqui e ali.
Vamos dar uma olhada no script anterior.
#! /usr/bin/python3 -u
# -*- encoding: utf-8 -*-
"""
DNS updater. It checks if hostname and IP are updated
on maps. If not, update accordingly and reload bind.
"""
# grep "\/\?update_dyndns=" /var/log/apache2/dyndns-access.log | sed "s/ HTTP.*//" | awk '{print $1, $NF}'
import re
import time
import os
import subprocess
import sys
LOG = "/var/log/apache2/dyndns-access.log"
DNSCONF = "/etc/bind/master/dyndns.truta.org"
DNSDATE = "/etc/bind/master/db.truta.org"
DNSFILES = [ DNSDATE, "/etc/bind/master/db.linux-br.org" ]
TRANSLATE = {
"goosfraba" : "helio",
"raspberrypi-masoso" : "rpi0",
"raspberrypi1" : "rpi1",
"raspberrypi2" : "rpi2",
"raspberrypi3" : "rpi3"
}
def debug(*msg):
if "DEBUG" in os.environ:
print("DEBUG:", *msg)
def get_latest_ips():
"""
Dictionary in format:
Domain/node : [ ipv4, ipv6]
"""
debug("get_latest_ips()")
NODE_IP = {}
with open(LOG) as logfile:
for line in logfile.readlines():
if not re.search("\/\?update_dyndns=", line):
continue
line = re.sub(" HTTP.*", "", line)
param = line.split()
ip = param[0]
nodename = re.sub("\/\?update_dyndns=", "", param[-1])
if not nodename in NODE_IP:
NODE_IP[nodename] = [ None, None ]
# ipv4?
if re.search("([0-9].*)\.([0-9].*)\.([0-9].*)\.([0-9].*)", ip):
NODE_IP[nodename][0] = ip
debug(f"{nodename} (ipv4): {ip}")
else:
NODE_IP[nodename][1] = ip
debug(f"{nodename} (ipv6): {ip}")
return NODE_IP
def apply_translate(DIC):
debug("apply_translate()")
# DIC = { translatedName : [ipv4, ipv6] }
debug("apply_translate(): DIC:\n", DIC)
tempDIC = {}
for nodename in DIC:
debug(f"apply_translate(): searching {nodename}")
if not nodename in TRANSLATE:
debug(f"{nodename} not in TRANSLATE table")
continue
dnsName = TRANSLATE[nodename]
if dnsName is None:
debug(f"{nodename} translates to None")
continue
nodeValue = DIC[nodename]
debug(f"apply_translate(): adding {dnsName} as {nodeValue}")
tempDIC[dnsName] = nodeValue
debug("apply_translate(): returning tempDIC:\n", tempDIC)
return tempDIC
def check_for_update(DIC):
debug("check_for_update()")
isupdated = False
buf = ""
debug("check_for_update(): current DNS entries")
with open(DNSCONF) as fh:
for line in fh.readlines():
if line.startswith(";") or line.startswith("#"):
buf += line
continue
line = line.rstrip()
param = line.split()
if len(param) != 3:
buf += f"{line}\n"
continue
subname, subtype, subip = param
print(f"DNS current entry: {subname}, Type: {subtype}, IP: {subip}")
if not subname in DIC:
debug(f"check_for_update(): {subname} is not into the DIC")
buf += f"{line}\n"
continue
ipv4, ipv6 = DIC[subname]
print(f"Found {subname} for update")
# ipv4
if subtype == "A":
print("Checking IPv4")
if ipv4 != subip and len(ipv4) > 0:
print(f" * Updating {subname} from {subip} to {ipv4}")
isupdated = True
buf += f"{subname}\t\t\t{subtype}\t{ipv4}\n"
continue
# ipv6
if subtype == "AAAA":
print("Checking IPv6")
if ipv6 != subip and len(ipv6) > 0:
print(f" * Updating {subname} from {subip} to {ipv6}")
isupdated = True
buf += f"{subname}\t\t\t{subtype}\t{ipv6}\n"
continue
buf += f"{line}\n"
if isupdated:
print(f"Updating file {DNSCONF}")
debug("check_for_update(): buf:", buf)
with open(DNSCONF, 'w') as fh:
fh.write(buf)
return isupdated
def get_dns_map_serial(dns_file: str) -> str:
with open(dns_file) as fh:
for line in fh.readlines():
if not re.search(" ; serial", line):
continue
timestamp = line.split()[0]
return timestamp
raise Exception(f"Failed to read serial from dns file: {dns_file}")
def get_serial_date_update(timestamp: str) -> list:
if len(timestamp) != 10 :
raise Exception(f"Wrong timestamp size (not 10): {timestamp}")
dateformat = timestamp[:8]
serial = timestamp[8:]
return (serial)
def update_serial_in_file(old_serial: str, new_serial: str, filename: str) -> None:
with open(filename) as fh:
buf = fh.read()
buf = re.sub(old_serial, new_serial, buf)
with open(filename, 'w') as fw:
fw.write(buf)
def update_timestamp(dryrun=False):
debug("update_timestamp()")
buf = ""
today = time.strftime("%Y%m%d", time.localtime())
timestamp = get_dns_map_serial(DNSDATE)
(last_update, last_serial) = get_serial_date_update(timestamp)
print("Timestamp:", timestamp)
print("Last update:", last_update)
print("Last serial:", last_serial)
original_timestamp = ""
if int(today) > int(last_update):
timestamp = f"{today}00"
else:
serial = int(last_serial) + 1
if serial < 100:
timestamp = "%s%02d" % (today, serial)
else:
timestamp = "%08d00" % (int(today) + 1)
print("New timestamp:", timestamp)
print("Updating file", DNSDATE)
debug(f"update_timestamp(): timestamp={timestamp}")
if dryrun is False:
update_serial_in_file(original_timestamp, timestamp, DNSDATE)
else:
print("-= dry-run mode =-")
print(f"Here file {DNSDATE} would be updated.")
print("Content:\n", buf)
def bind_restart(dryrun=False):
#cmd = "systemctl restart named.service"
## reload is enough
cmd = "systemctl reload named.service"
if dryrun is False:
subprocess.call(cmd.split())
else:
print("-= dry-run mode =-")
print(f"Here command would be called: {cmd}")
def main():
now = time.ctime(time.time())
print(f"Starting: {now}")
DNS = get_latest_ips()
DNS = apply_translate(DNS)
debug(DNS)
status = check_for_update(DNS)
if status:
if len(sys.argv) > 1 and sys.argv[-1] == "--help":
print(f"Use: {sys.argv[0]} [--help|--dry-run]")
sys.exit(0)
if len(sys.argv) > 1 and sys.argv[-1] == "--dry-run":
doDryRun = True
else:
doDryRun = False
update_timestamp(dryrun=doDryRun)
bind_restart(dryrun=doDryRun)
if __name__ == '__main__':
main()
O código está gigante e uma zona. E estava dando crash. Vamos então olhar por partes.
LOG = "/var/log/apache2/dyndns-access.log"
DNSCONF = "/etc/bind/master/dyndns.truta.org"
DNSDATE = ""
DNSFILES = [ DNSDATE, "/etc/bind/master/db.linux-br.org" ]
LOG
é onde eu tenho de ler pra buscar o padrão de acesso.
DNSCONF
é onde ficam as definições de "IP TIPO NOME" como "1.2.3.4 A helio" pro caso de IPv4.
DNSDATE
é onde ficavam as atualizações pro domínio truta.org.
Eu percebi que dava pra fazer a mesma coisa com outros domínios, então depois
eu inseri junto o DNSFILES
pra ter também o linux-br.org.
No final esses aquivos só precisam ser abertos pra alterar o serial.
O mapa de dns, pra entender o que é, é esse aqui pro truta.org:
❯ cat /etc/bind/master/db.truta.org
$TTL 180 ; # three minute
@ IN SOA ns1.truta.org. helio.loureiro.eng.br. (
2025080800 ; serial
43200 ; refresh (12 hours)
3600 ; retry (1 hour)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
@ NS ns1.truta.org.
@ NS ns2.afraid.org.
@ NS ns1.first-ns.de.
@ NS robotns2.second-ns.de.
@ NS robotns3.second-ns.com.
@ A 95.216.213.181
@ MX 5 mail.truta.org.
@ AAAA 2a01:4f9:c012:f3c4::1
@ TXT "v=spf1 a mx ip4:95.216.213.181 ip6:2a01:4f9:c012:f3c4::1 include:_spf.google.com -all"
mail._domainkey IN TXT "v=DKIM1; k=rsa; t=y; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpp/HlB1QAHjp6jHGCj9kfcZSjY3ipbGuU/enVjEeptlNgw2qTxRRJ6puPiW5DWCOaPEg2nC//wRazFgMuLZ3C89vF2TT57upngmNuCcLtbwNxo6G1JFo5GD92UbQgstYC+cjfrRlw56XfwzWVtojR4ZCB8PCQCguwhwunwRhJEwIDAQAB" ; ----- DKIM key mail for truta.org
; As e AAAAs
; CNAMES
www AAAA 2a01:4f9:c012:f3c4::1
www A 95.216.213.181
ns1 A 95.216.213.181
AAAA 2a01:4f9:c012:f3c4::1
mail A 95.216.213.181
AAAA 2a01:4f9:c012:f3c4::1
; DYNAMIC TRUTAS
$INCLUDE "/etc/bind/master/dyndns.truta.org";
A linha que precisa ser alterada é a que tem 2025080800 ; serial
.
A parte que faz a mágica de ler os novos endereços IPs é $INCLUDE "/etc/bind/master/dyndns.truta.org";
.
E se não está familiarizado com mapas de dns, o que tem depois do ";" é ignorado.
Então o bind lê somente o 2025080800
.
O ; serial
é um comentário pra humanos entenderem o que é aquilo.
O serial é um número inteiro.
Você pode usar qualquer número.
Só precisa incrementar quando faz alguma alteração no mapa pra poder notificar que houve mudança pro seus secundários.
A boa prática é usar YYYYMMDDXX
onde:
Então se o mapa é atualizado no mesmo dia, você incrementa o XX. Claro que isso te limita à 100 alterações por dia, de 0 a 99. Mas não é algo como ficar atualizando tanto o mapa. A menos que você seja um provedor de cloud. Daí as coisas funcionam de jeito beeeeem diferente. Mas vamos ficar no feijão com arroz.
Vamos voltar ao script. Como está escrito com funções, fica mais fácil de olhar pra cada uma independentemente. Eu vou colocar fora de ordem pra ficar mais claro o que faz o quê. Ou o que deveria fazer o quê.
def debug(*msg):
if "DEBUG" in os.environ:
print("DEBUG:", *msg)
Isso é claramente uma gambiarra. XGH em sua melhor forma. Só pra eu ter um "debug" pra olhar dentro do programa.
def main():
now = time.ctime(time.time())
print(f"Starting: {now}")
DNS = get_latest_ips()
DNS = apply_translate(DNS)
debug(DNS)
status = check_for_update(DNS)
if status:
if len(sys.argv) > 1 and sys.argv[-1] == "--help":
print(f"Use: {sys.argv[0]} [--help|--dry-run]")
sys.exit(0)
if len(sys.argv) > 1 and sys.argv[-1] == "--dry-run":
doDryRun = True
else:
doDryRun = False
update_timestamp(dryrun=doDryRun)
bind_restart(dryrun=doDryRun)
if __name__ == '__main__':
main()
A função main é o que chama tudo.
Algumas coisas de tempo pra saber quando começa.
E efetivamente a primeira chamada aqui: DNS = get_latest_ips()
.
Soa razoável olhar pra esse get_latest_ips()
e imaginar que volta os últimos ips.
Mas ip de quem?
Com certeza tem de voltar algo como um dicionário pra você saber qual hostname tem qual ip.
E esse DNS
?
Tinha de ser em maiúsculo?
Definitivamente não.
Variáveis em maiúsculo a gente deixa pra constantes como LOG
.
Mas seguimos...
Agora temos um DNS = apply_translate(DNS)
.
Esse é um filtro que usa o TRANSLATE
lá em cima:
TRANSLATE = {
"goosfraba" : "helio",
"raspberrypi-masoso" : "rpi0",
"raspberrypi1" : "rpi1",
"raspberrypi2" : "rpi2",
"raspberrypi3" : "rpi3"
}
E isso é usado porque meu desktop, por exemplo, tem hostname "goosfraba". Mas eu quero que ele seja no fqdn como "helio.truta.org". Então eu uso essa "tradução" de hostname recebido pro que eu quero no dns.
status = check_for_update(DNS)
eu provavelmente comparo com o que está em DNSCONF
.
E se houve alteração, salvo em status.
Definitivamente um boolean.
Em seguinda essa parte aqui:
if status:
if len(sys.argv) > 1 and sys.argv[-1] == "--help":
print(f"Use: {sys.argv[0]} [--help|--dry-run]")
sys.exit(0)
if len(sys.argv) > 1 and sys.argv[-1] == "--dry-run":
doDryRun = True
else:
doDryRun = False
Esse é um outro XGH. Tudo pra ver se existe o parâmetro "--help". Ou se é passado "--dry-run". "dry-run" é como é chamado um "test pra ver se vai" sem realmente fazer nada. Eu provavelmente fiz isso porque a primeira implementação era em shell script. Mas mesmo em shell script isso seria XGH.
update_timestamp(dryrun=doDryRun)
faz aquele update de serial nos mapas de dns se algo mudou.
A menos que o parâmetro de "dry-run" esteja lá.
E finalmente bind_restart(dryrun=doDryRun)
reinicia o serviço via systemd ser não for um "dry-run".
Olhamos a lógica do que deveria fazer.
Agora vamos ver o que foi feito.
Vamos começar com o get_latest_ips( )
.
def get_latest_ips():
"""
Dictionary in format:
Domain/node : [ ipv4, ipv6]
"""
debug("get_latest_ips()")
NODE_IP = {}
with open(LOG) as logfile:
for line in logfile.readlines():
if not re.search("\/\?update_dyndns=", line):
continue
line = re.sub(" HTTP.*", "", line)
param = line.split()
ip = param[0]
nodename = re.sub("\/\?update_dyndns=", "", param[-1])
if not nodename in NODE_IP:
NODE_IP[nodename] = [ None, None ]
# ipv4?
if re.search("([0-9].*)\.([0-9].*)\.([0-9].*)\.([0-9].*)", ip):
NODE_IP[nodename][0] = ip
debug(f"{nodename} (ipv4): {ip}")
else:
NODE_IP[nodename][1] = ip
debug(f"{nodename} (ipv6): {ip}")
return NODE_IP
Aparentemente a estrutura NODE_IP = {}
vai ser um dicionário do tipo
hostname: [IPv4][IPv6]
.
with open(LOG) as logfile:
for line in logfile.readlines():
if not re.search("\/\?update_dyndns=", line):
continue
Aqui é ler o aquivo LOG
.
Se a linha não tiver o padrão "/?update_dynds=", vai pra próxima linha.
line = re.sub(" HTTP.*", "", line)
param = line.split()
ip = param[0]
nodename = re.sub("\/\?update_dyndns=", "", param[-1])
A alinha recebida é algo assim:
83.233.219.150 - - [11/Aug/2025:08:54:57 +0000] "GET /?update_dyndns=goosfraba HTTP/1.1" 200 87144 "-" "curl/8.15.0"
Então o regex em line = re.sub(" HTTP.*", "", line)
remove tudo depois de " HTTP", including essa parte junto.
A linha então fica algo assim:
83.233.219.150 - - [11/Aug/2025:08:54:57 +0000] "GET /?update_dyndns=goosfraba
A linha param = line.split()
cria a variável param (parâmetros) com a linha separada por espaços simples.
ip = param[0]
pega o primeiro parâmetro.
No exemplo seria "83.233.219.150".
nodename = re.sub("\/\?update_dyndns=", "", param[-1])
pega o último parâmetro, que no exemplo seria
"/?update_dyndns=goosfraba" e remove a parte "/?update_dyndns=".
Vai sobrar o hostname, "goosfraba".
if not nodename in NODE_IP:
NODE_IP[nodename] = [ None, None ]
Aqui olha se o dicionário NODE_IP
tem a chave que é o nodename.
Eu comentei acima hostname, mas no código chamei de nodename.
Tenha em mente que eram a mesma coisa.
E se a chave não existe, eu crio com uma lista vazia.
if re.search("([0-9].*)\.([0-9].*)\.([0-9].*)\.([0-9].*)", ip):
NODE_IP[nodename][0] = ip
debug(f"{nodename} (ipv4): {ip}")
else:
NODE_IP[nodename][1] = ip
debug(f"{nodename} (ipv6): {ip}")
Finalmente eu comparo o ip se bate com o padrão ([0-9].*)\.([0-9].*)\.([0-9].*)\.([0-9].*)
que
é basicamente qualquer número de 0 à 9 seguido de ponto.
Então se eu recebesse um IP inválido, como 999.9999999.999999999999.99999999, validaria.
Mas pro script XGH, foi suficiente.
Daí eu sei se é um IPv4.
Se não for, é IPv6.
Como vou lendo o arquivo LOG
de cima pra baixo,
esses valores de hostname e ip é atualizado várias vezes.
Com os mesmos valores.
E como o servidor web atualiza também de cima pra baixo, a última linha deve ser a mais atual.
Não muito eficiente, mas dá certo.
XGH raiz.
O apply_translate( )
eu não vou comentar.
Já expliquei acima o que ele faz, que é traduzir hostname pra entrada no dns.
Não adiciona muita coisa.
Então vamos pro próximo.
O próximo é um XGH raiz.
Função grande.
É a check_for_update( )
.
Vamos por partes.
def check_for_update(DIC):
debug("check_for_update()")
isupdated = False
buf = ""
debug("check_for_update(): current DNS entries")
with open(DNSCONF) as fh:
for line in fh.readlines():
if line.startswith(";") or line.startswith("#"):
buf += line
continue
line = line.rstrip()
param = line.split()
if len(param) != 3:
buf += f"{line}\n"
continue
Aqui é lido o arquivo DNSCONF
, que é onde está o mapa "ip tipo nome" como "1.2.3.4 A helio".
Essa primeira parte descarta as linhas que começam ou com ";" ou com "#".
Na verdade copia a linha como está em buf
.
A parte de baixo prepara a linha pra ser lida se tiver o padrão de ter ao menos 3 parâmetros quando separada por espaços.
Se não for, copie pro buffer e vá pra próxima linha.
subname, subtype, subip = param
print(f"DNS current entry: {subname}, Type: {subtype}, IP: {subip}")
if not subname in DIC:
debug(f"check_for_update(): {subname} is not into the DIC")
buf += f"{line}\n"
continue
Aqui já pega os 3 parâmetros que serão subname, subtype, subip
.
O subname
é o hostname.
Ou nodename.
Consistência não é meu forte.
Ou não era.
E se esse hostname não estiver no dicionário, salva a linha como ela era no buffer e vai pra próxima linha.
ipv4, ipv6 = DIC[subname]
print(f"Found {subname} for update")
# ipv4
if subtype == "A":
print("Checking IPv4")
if ipv4 != subip and len(ipv4) > 0:
print(f" * Updating {subname} from {subip} to {ipv4}")
isupdated = True
buf += f"{subname}\t\t\t{subtype}\t{ipv4}\n"
continue
# ipv6
if subtype == "AAAA":
print("Checking IPv6")
if ipv6 != subip and len(ipv6) > 0:
print(f" * Updating {subname} from {subip} to {ipv6}")
isupdated = True
buf += f"{subname}\t\t\t{subtype}\t{ipv6}\n"
continue
buf += f"{line}\n"
Aqui os endereços IPv4 e IPv6 são recuperados do dicionário em ipv4, ipv6 = DIC[subname]
.
Se o subtipo for "A", eu olho se o endereço IPv4 mudou.
Se for "AAAA", olho o IPv6.
Se alterar, eu altero isupdated
pra verdadeiro.
if isupdated:
print(f"Updating file {DNSCONF}")
debug("check_for_update(): buf:", buf)
with open(DNSCONF, 'w') as fh:
fh.write(buf)
return isupdated
E finalmente, se o código foi alterado, escrevo em cima do arquivo antigo o conteúdo novo.
Até aqui tudo bem.
O código está meio rebuscado e com alguns pontos de XGH mas parece funcional.
Vamos então olhar a próxima parte, que é atualizar os mapas caso tenha tido alguma mudança.
Olhando pra update_timestamp( )
vamos ver que esse depende de outras funções, que ficam pra cima no código.
E aqui já entra um pequeno mal estar.
Eu gosto dos conselhos do Uncle Bob sobre clean code.
E prefiro ler um código de cima pra baixo.
Usou uma função ou método?
Coloque abaixo.
Na ordem em que foi chamado.
Mas com python com funções isso não funciona.
Você tem de colocar acima as funções que serão chamadas abaixo.
Então vamos lá.
def update_timestamp(dryrun=False):
debug("update_timestamp()")
buf = ""
today = time.strftime("%Y%m%d", time.localtime())
timestamp = get_dns_map_serial(DNSDATE)
Bem básico.
Pega o dia de hoje no formato "YYYMMDD".
Em seguida olha qual o serial do arquivo com get_dns_map_serial( )
.
Ao contrário do que fiz até agora, eu vou parar de analizar essa função e seguir o código que ela chamou.
def get_dns_map_serial(dns_file: str) -> str:
with open(dns_file) as fh:
for line in fh.readlines():
if not re.search(" ; serial", line):
continue
timestamp = line.split()[0]
return timestamp
raise Exception(f"Failed to read serial from dns file: {dns_file}")
Esse trecho de código é claramente mais recente. Usando f-strings e mais type hints pra saber o que seria ali. E variáveis em minúsculas! E bem simples: abre o arquivo passado (um mapa de dns) e busca pela linha com "; serial". Quebra a linha em espaços e retorna o primeiro parâmetro encontrado. Não tem verificação se realmente voltou uma string, mas parece ser isso. E se não achar nada, lança uma exceção e para a execução toda. Um pouco demais, mas pelo menos não retorna algo errado e deixa destruir o mapa de dns.
Vamos então voltar a olhar a função chamadora, update_timestamp( )
.
(last_update, last_serial) = get_serial_date_update(timestamp)
Novamente um código mais enxuto.
Ao invés de manter no corpo da função, chamar outra função auto-explicativa.
Então vamos olhar a get_serial_date_update( )
.
def get_serial_date_update(timestamp: str) -> list:
if len(timestamp) != 10 :
raise Exception(f"Wrong timestamp size (not 10): {timestamp}")
dateformat = timestamp[:8]
serial = timestamp[8:]
return (serial)
Aqui ele verifica se o serial tem 10 caractéres, ou 10 dígitos com if len(timestamp) != 10
.
Ou gera uma exceção e encerra a execução.
Pega os primeiro 8 caractéres e coloca em dateformat
.
Em seguida pega os caractéres restantes e coloca em serial
.
E retorna o serial
.
Perceberam os problemas?
Já começa que a função chama-se get_serial_date_update( )
.
Qual o update?
E deveria retornar uma lista.
Até volta, mas com um valor só, o serial
.
A função que chamava esperava o quê?
Esperava isso aqui: (last_update, last_serial) = get_serial_date_update(timestamp)
.
Então recebe last_update
mas não last_serial
.
Achamos um 🪲.
Vamos então continuar em update_timestamp( )
.
print("Timestamp:", timestamp)
print("Last update:", last_update)
print("Last serial:", last_serial)
original_timestamp = ""
if int(today) > int(last_update):
timestamp = f"{today}00"
else:
serial = int(last_serial) + 1
if serial < 100:
timestamp = "%s%02d" % (today, serial)
else:
timestamp = "%08d00" % (int(today) + 1)
print("New timestamp:", timestamp)
Aqui a data today
é transformada em int (integer, inteiro) e comparada com last_update
, que também é transformado em int.
Se for maior, mais atual, então inicializa com "00".
Se não for mais atual, então incrementa o número serial em 1.
E olha se for maior que 100, incializa o dia seguinte e começa com 00 de novo.
E finalmente:
if dryrun is False:
update_serial_in_file(original_timestamp, timestamp, DNSDATE)
Se não está rodando em dry-run, rodar esse update_serial_in_file( )
.
Pelo nome é possível deduzer que seja trocar o serial antigo do arquivo pelo novo.
Mas vamos olhar o código.
def update_serial_in_file(old_serial: str, new_serial: str, filename: str) -> None:
with open(filename) as fh:
buf = fh.read()
buf = re.sub(old_serial, new_serial, buf)
with open(filename, 'w') as fw:
fw.write(buf)
E sim, é basicamente isso mesmo. Ler o arquivo todo, trocar a string antiga pela nova, e salvar.
E com isso chegamos na parte final do script:
bind_restart(dryrun=doDryRun)
É bem descritivo e faz exatamente o que diz: reinicia o serviço de dns via systemd.
def bind_restart(dryrun=False):
#cmd = "systemctl restart named.service"
## reload is enough
cmd = "systemctl reload named.service"
if dryrun is False:
subprocess.call(cmd.split())
else:
print("-= dry-run mode =-")
print(f"Here command would be called: {cmd}")
Se teve paciência de ler até aqui, espero que tenha gostado e aprendido que esse código estava bem ruim. E o que seria um código bom? Com certeza um que seja legível de cima pra baixo. Até certo ponto porque python não é tão flexível assim. Sem delongas, vou mostrar o código novo. Depois vou explicando.
#! /usr/bin/python3 -u
# -*- encoding: utf-8 -*-
"""
DNS updater. It checks if hostname and IP are updated
on maps. If not, update accordingly and reload bind.
"""
# grep "\/\?node=" /var/log/apache2/dyndns-access.log | sed "s/ HTTP.*//" | awk '{print $1, $NF}'
import re
import time
import os
import subprocess
import sys
import argparse
import logging
logger = logging.getLogger(__file__)
consoleOutputHandler = logging.StreamHandler()
logger.addHandler(consoleOutputHandler)
logger.setLevel(logging.INFO)
TRANSLATE = {
"goosfraba" : "helio",
"raspberrypi-masoso" : "rpi0",
"raspberrypi1" : "rpi1",
"raspberrypi2" : "rpi2",
"raspberrypi3" : "rpi3"
}
class DnsIPUpdate:
'''
A class that reads httpd logs for patterns in order to find a host IP,
update dns file accordingly if needed and restart dns service if updated.
'''
def __init__(self, args: argparse.Namespace):
self._logfile: str = args.logfile
self._bindfiles: list = args.bindfiles.split(",")
self._IPv4: dict = {}
self._IPv6: dict = {}
self._dryrun: bool = args.dryrun
self._dyndnsfile: str = args.dyndnsfile
self._fakesystemd: bool = args.fakesystemd
# for the serial on the dns files
self._year: str = time.strftime("%Y", time.localtime())
self._month: str = time.strftime("%m", time.localtime())
self._day: str = time.strftime("%d", time.localtime())
def update(self):
'run the update'
matched_lines: list = self.getEntriesFromLogs()
self._populateIPsData(matched_lines)
logger.debug(f'IPv4s: {self._IPv4}')
logger.debug(f'IPv6s: {self._IPv6}')
self._applyTranslation()
logger.debug(f'IPv4s: {self._IPv4}')
logger.debug(f'IPv6s: {self._IPv6}')
if not self._dryrun:
resp: bool = self._updateDNSFiles()
if resp is True:
self._restartService()
def getEntriesFromLogs(self) -> list:
'return the matching lines from logging file'
matches: list = []
with open(self._logfile) as fd:
for line in fd.readlines():
if not re.search(r"\/\?update_dyndns=", line):
continue
line = line.rstrip()
logger.debug(f"found line in log: {line}")
matches.append(line)
return matches
def _populateIPsData(self, matches):
'fill the IPv4 and IPv6 structures with IPs and hostnames'
for line in matches:
ip_addr, _, _, timestamp, timezone, method, uri, http_version, status_code, size, _, user_agent = line.split()
timestamp = timestamp[1:]
timezone = timezone[:-1]
method = method[1:]
http_version = http_version[:-1]
user_agent = user_agent[1:-1]
logger.debug(f"ip={ip_addr} timestamp={timestamp} timezone={timezone} method={method} uri={uri} proto={http_version} status_code={status_code} size={size} agent={user_agent}")
hostname = self._getHostName(uri)
if self._isIPv4(ip_addr):
self._IPv4[hostname] = ip_addr
else:
self._IPv6[hostname] = ip_addr
def _isIPv4(self, ip: str) -> bool:
'quickly finds out whether IPv4 or not - then IPv6'
if re.search("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ip):
return True
return False
def _getHostName(self, uri: str) -> str:
'filter uri to return only the hostname'
hostname = re.sub(r",.*", "", uri)
hostname = re.sub(r".*update_dyndns=", "", hostname)
return hostname
def _applyTranslation(self) -> None:
'translate hostname according to table'
for old_name, new_name in TRANSLATE.items():
if old_name in self._IPv4:
logger.debug(f"Translating IPv4: from={old_name} to={new_name}")
self._IPv4[new_name] = self._IPv4[old_name]
del self._IPv4[old_name]
if old_name in self._IPv6:
logger.debug(f"Translating IPv6: from={old_name} to={new_name}")
self._IPv6[new_name] = self._IPv6[old_name]
del self._IPv6[old_name]
def _updateDNSFiles(self) -> bool:
'check and update DNS files if needed'
isUpdated = self._updateDynDNS()
if isUpdated is True:
self._updateSerialOnDNS()
return isUpdated
def _updateDynDNS(self) -> bool:
'check each entry into dyndns file and returns true if updated'
status = False # by default we don't update
output = []
with open(self._dyndnsfile) as fd:
buffer = fd.readlines()
for line in buffer:
line = line.rstrip() # remove new line
try:
hostname, _, _, dns_type, ip_addr = line.split("\t")
except ValueError:
output.append(f"{line}\n")
continue
match dns_type:
case "A":
# IPv4
if hostname in self._IPv4:
if self._IPv4[hostname] != ip_addr:
logger.info(f"updating IPv4 for {hostname}: old={ip_addr} new={self._IPv4[hostname]}")
line = f"{hostname}\t\t\tA\t{self._IPv4[hostname]}"
status = True
case "AAAA":
# IPv6
if hostname in self._IPv6:
if self._IPv6[hostname] != ip_addr:
logger.info(f"updating IPv6 for {hostname}: old={ip_addr} new={self._IPv6[hostname]}")
line = f"{hostname}\t\t\tAAAA\t{self._IPv6[hostname]}"
status = True
case _:
pass
output.append(f"{line}\n")
if status is True:
with open(self._dyndnsfile, 'w') as fw:
fw.write("".join(output))
return status
def _updateSerialOnDNS(self) -> None:
for filename in self._bindfiles:
buffer: list = []
logger.debug(f"updating serial on file: {filename}")
with open(filename) as fd:
for line in fd.readlines():
if not re.search("; serial", line):
buffer.append(line)
continue
line = line.rstrip()
logger.debug(f"line with serial number: {line}")
serial = self._sanitizeSerial(line)
logger.debug(f"serial={serial}")
new_serial = self._increaseSerial(serial)
logger.debug(f"new_serial={new_serial}")
line = f"\t\t\t\t{new_serial} ; serial\n"
logger.info(f"updating: filename={filename} old_serial={serial} new_serial={new_serial}")
buffer.append(line)
with open(filename, "w") as fw:
fw.write("".join(buffer))
def _sanitizeSerial(self, serial_nr: str) -> str:
'to remove the \t and spaces'
serial, _ = serial_nr.split(";")
serial = re.sub(r"\t", "", serial)
serial = re.sub(" ", "", serial)
return serial
def _increaseSerial(self, serial_nr: str):
'to identify and increase serial'
year = serial_nr[:4]
month = serial_nr[4:6]
day = serial_nr[6:8]
counter = serial_nr[8:]
today = f"{self._year}{self._month}{self._day}"
serial_date = f"{year}{month}{day}"
if today == serial_date:
new_counter = self._increaseString(counter)
return f"{today}{new_counter}"
else:
return f"{today}00"
def _increaseString(self, str_number: str) -> str:
'convert string to int, increase by one and return as string'
int_number: int = int(str_number)
int_number += 1
return f"{int_number}"
def _restartService(self) -> None:
'restart systemd service'
cmd = ["systemctl", "reload", "named.service"]
logger.info("restarting named.service")
if self._fakesystemd is True:
print(f"here service would be restarted: {cmd}")
else:
subprocess.call(cmd)
def getTimestamp():
return time.strftime("%Y%m%dT%H:%M:%S", time.localtime())
def main():
logger.info(f"starting: {getTimestamp()}")
parser = argparse.ArgumentParser(description='script to update DNS entries for dynamic clients found on httpd logs')
parser.add_argument("--logfile", required=True, help="the httpd log to be checked")
parser.add_argument("--loglevel", default="info", help="logging level for this script")
parser.add_argument("--bindfiles", required=True, help="files separated by \",\" to be updated to update serial")
parser.add_argument("--dyndnsfile", required=True, help="the dyndns map to update the IPs")
parser.add_argument("--dryrun", default=False, type=bool, help='run as dry-run or not (default=false)')
parser.add_argument("--fakesystemd", default=False, type=bool, help='run just a printout instead of systemd')
args = parser.parse_args()
if args.loglevel != "info":
logger.setLevel(args.loglevel.upper())
logger.debug(f"args: {args}")
dns = DnsIPUpdate(args)
dns.update()
logger.info(f"finished: {getTimestamp()}")
if __name__ == '__main__':
main()
A parte de baixo continua mais ou menos igual: chama uma função main( )
que faz tudo.
Mas agora a função main( )
usa argparse
pra pegar os argumentos.
Então as constantes com os nomes dos arquivos sumiram e viraram agora argumentos.
Isso facilita escrever testes (o que não fiz ainda).
O que o código faz?
Faz isso aqui:
dns = DnsIPUpdate(args)
dns.update()
Cria um objeto DnsIPUpdate( )
e chama o método update( )
.
Não mais funções mas métodos.
Parece mais simples de entender.
Espero.
Vamos então agora olhar a classe. Começando com sua inicialização.
def __init__(self, args: argparse.Namespace):
self._logfile: str = args.logfile
self._bindfiles: list = args.bindfiles.split(",")
self._IPv4: dict = {}
self._IPv6: dict = {}
self._dryrun: bool = args.dryrun
self._dyndnsfile: str = args.dyndnsfile
self._fakesystemd: bool = args.fakesystemd
# for the serial on the dns files
self._year: str = time.strftime("%Y", time.localtime())
self._month: str = time.strftime("%m", time.localtime())
self._day: str = time.strftime("%d", time.localtime())
As variáveis (atributos da classe) vêm do argparse. Todas com o "_" no início pra indicar que são internas. Não que isso importe muito uma vez que não é uma classe feita pra ser usada como biblioteca. E agora IPv4 e IPv6 são estruturas separadas. Do tipo dicionário. E pegamos ano, mês e dia separados.
Vamos então olhar o método chamado, o update( )
:
def update(self):
'run the update'
matched_lines: list = self.getEntriesFromLogs()
self._populateIPsData(matched_lines)
logger.debug(f'IPv4s: {self._IPv4}')
logger.debug(f'IPv6s: {self._IPv6}')
self._applyTranslation()
logger.debug(f'IPv4s: {self._IPv4}')
logger.debug(f'IPv6s: {self._IPv6}')
if not self._dryrun:
resp: bool = self._updateDNSFiles()
if resp is True:
self._restartService()
Primeiramente que o método update( )
aparece logo abaixo da inicialização da classe.
Então você lê o código de cima pra baixo, o que é muito desejado.
E o código chama outro método em matched_lines: list = self.getEntriesFromLogs( )
, que vai retornar uma lista.
Vamost então olhar o que faz getEntriesFromLogs( )
.
def getEntriesFromLogs(self) -> list:
'return the matching lines from logging file'
matches: list = []
with open(self._logfile) as fd:
for line in fd.readlines():
if not re.search(r"\/\?update_dyndns=", line):
continue
line = line.rstrip()
logger.debug(f"found line in log: {line}")
matches.append(line)
return matches
Ele então abre o arquivo _logfile
e guarda as linhas com o padrão "/?update_dyndns=".
E em formate de lista.
Depois retorna esses valores.
Não melhorou muito em termos de ler o arquivo todo e fazer o parsing de tudo.
Ficou só mais legível.
Voltando ao update( )
temos logo em seguida: self._populateIPsData(matched_lines)
.
Então vamos olhar o código de _populateIPsData( )
.
def _populateIPsData(self, matches):
'fill the IPv4 and IPv6 structures with IPs and hostnames'
for line in matches:
ip_addr, _, _, timestamp, timezone, method, uri, http_version, status_code, size, _, user_agent = line.split()
timestamp = timestamp[1:]
timezone = timezone[:-1]
method = method[1:]
http_version = http_version[:-1]
user_agent = user_agent[1:-1]
logger.debug(f"ip={ip_addr} timestamp={timestamp} timezone={timezone} method={method} uri={uri} proto={http_version} status_code={status_code} size={size} agent={user_agent}")
hostname = self._getHostName(uri)
if self._isIPv4(ip_addr):
self._IPv4[hostname] = ip_addr
else:
self._IPv6[hostname] = ip_addr
Agora a linha é quebrada em espaços mas o dados são utilizados. O que é "_" significa que seja descartado. Então olhando a linha de exemplo novamente:
83.233.219.150 - - [11/Aug/2025:08:54:57 +0000] "GET /?update_dyndns=goosfraba HTTP/1.1" 200 200 "-" "curl/8.15.0"
Temos:
O que vem a seguir é somente pra limpar esses parâmetros.
Servem pra alguma coisa?
Não.
Só pra ficar bonitinho no debug.
O que realmente importa são os valores de ip_addr
e uri
.
Em seguida é chamado hostname = self._getHostName(uri)
.
Vamos olhar o código do _getHostName( )
.
def _getHostName(self, uri: str) -> str:
'filter uri to return only the hostname'
hostname = re.sub(r",.*", "", uri)
hostname = re.sub(r".*update_dyndns=", "", hostname)
return hostname
É somente o mesmo sanitizador usado anteriormente. Vai remover o "/?update_dyndns=" e deixar somente o hostname.
Voltando ao _populateIPsData( )
, temos a chamada if self._isIPv4(ip_addr):
pra verificar se é um endereço IPv4 ou não.
def _isIPv4(self, ip: str) -> bool:
'quickly finds out whether IPv4 or not - then IPv6'
if re.search("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ip):
return True
return False
O código agora olha se é um número válido dentro do range de IPv4.
Então após essa verificação, ou o dicionário _IPv4 ou o _IPv6 é populado. E segue o formato "hostname: IP". Mas cada um em seu dicionário agora.
Voltando ao update( )
temos:
logger.debug(f'IPv4s: {self._IPv4}')
logger.debug(f'IPv6s: {self._IPv6}')
self._applyTranslation()
logger.debug(f'IPv4s: {self._IPv4}')
logger.debug(f'IPv6s: {self._IPv6}')
Que é a mesmo tradução feita anteriormente.
def _applyTranslation(self) -> None:
'translate hostname according to table'
for old_name, new_name in TRANSLATE.items():
if old_name in self._IPv4:
logger.debug(f"Translating IPv4: from={old_name} to={new_name}")
self._IPv4[new_name] = self._IPv4[old_name]
del self._IPv4[old_name]
if old_name in self._IPv6:
logger.debug(f"Translating IPv6: from={old_name} to={new_name}")
self._IPv6[new_name] = self._IPv6[old_name]
del self._IPv6[old_name]
Seguindo em frente no update( )
temos o seguinte:
if not self._dryrun:
resp: bool = self._updateDNSFiles()
Vamos olhar então _updateDNSFiles( )
que faz o update já visto anteriormente.
def _updateDNSFiles(self) -> bool:
'check and update DNS files if needed'
isUpdated = self._updateDynDNS()
if isUpdated is True:
self._updateSerialOnDNS()
return isUpdated
Ele chama _updateDynDNS( )
.
Vamos então ao código.
def _updateDynDNS(self) -> bool:
'check each entry into dyndns file and returns true if updated'
status = False # by default we don't update
output = []
with open(self._dyndnsfile) as fd:
buffer = fd.readlines()
for line in buffer:
line = line.rstrip() # remove new line
try:
hostname, _, _, dns_type, ip_addr = line.split("\t")
except ValueError:
output.append(f"{line}\n")
continue
Então ele abre o arquivo e lê linha por linha.
Mas dessa vez tenta popular a estrutura: hostname, _, _, dns_type, ip_addr = line.split("\t")
.
Se der errado, joga a linha no que será escrito depois.
E segue pra próxima linha.
Continuando.
match dns_type:
case "A":
# IPv4
if hostname in self._IPv4:
if self._IPv4[hostname] != ip_addr:
logger.info(f"updating IPv4 for {hostname}: old={ip_addr} new={self._IPv4[hostname]}")
line = f"{hostname}\t\t\tA\t{self._IPv4[hostname]}"
status = True
case "AAAA":
# IPv6
if hostname in self._IPv6:
if self._IPv6[hostname] != ip_addr:
logger.info(f"updating IPv6 for {hostname}: old={ip_addr} new={self._IPv6[hostname]}")
line = f"{hostname}\t\t\tAAAA\t{self._IPv6[hostname]}"
status = True
case _:
pass
Usa match
ao invés de um if
.
Poderia usar if
?
Sim.
Mas achei que o match
deixou o código mais organizado.
Em seguida verifica se o hostname existe no dicionário de IP.
Se existir, compara se o IP está diferente ou não.
Se estiver, atualiza a flag status
pra verdadeiro pra avisar que houveram mudanças.
output.append(f"{line}\n")
if status is True:
with open(self._dyndnsfile, 'w') as fw:
fw.write("".join(output))
return status
A parte final é salvar o arquivo caso tenha havido alguma alteração.
Voltando para _updateDNSFiles( )
, temos:
if isUpdated is True:
self._updateSerialOnDNS()
return isUpdated
Vamos então olhar _updateSerialOnDNS( )
:
def _updateSerialOnDNS(self) -> None:
for filename in self._bindfiles:
buffer: list = []
logger.debug(f"updating serial on file: {filename}")
with open(filename) as fd:
for line in fd.readlines():
if not re.search("; serial", line):
buffer.append(line)
continue
line = line.rstrip()
logger.debug(f"line with serial number: {line}")
serial = self._sanitizeSerial(line)
logger.debug(f"serial={serial}")
new_serial = self._increaseSerial(serial)
logger.debug(f"new_serial={new_serial}")
line = f"\t\t\t\t{new_serial} ; serial\n"
logger.info(f"updating: filename={filename} old_serial={serial} new_serial={new_serial}")
buffer.append(line)
with open(filename, "w") as fw:
fw.write("".join(buffer))
Aqui já virou feijão com arroz.
O que temos de olhar é _sanitizeSerial( )
,
depois _increaseSerial( )
.
def _sanitizeSerial(self, serial_nr: str) -> str:
'to remove the \t and spaces'
serial, _ = serial_nr.split(";")
serial = re.sub(r"\t", "", serial)
serial = re.sub(" ", "", serial)
return serial
Sem surpresas, só um regex pra voltar o número serial.
def _increaseSerial(self, serial_nr: str):
'to identify and increase serial'
year = serial_nr[:4]
month = serial_nr[4:6]
day = serial_nr[6:8]
counter = serial_nr[8:]
today = f"{self._year}{self._month}{self._day}"
serial_date = f"{year}{month}{day}"
if today == serial_date:
new_counter = self._increaseString(counter)
return f"{today}{new_counter}"
else:
return f"{today}00"
Aqui o código mudou.
Ao invés de comparar com números inteiros, faz uma comparação de string direto.
E chama _increaseString( )
pra aumentar o valor da string.
def _increaseString(self, str_number: str) -> str:
'convert string to int, increase by one and return as string'
int_number: int = int(str_number)
int_number += 1
return f"{int_number}"
Aqui sim a string é convertida pra inteiro. Esse é incrementado por 1. Mas retorna como string.
Chegamos então na parte final do update( )
que chama o método _restartService()
.
def _restartService(self) -> None:
'restart systemd service'
cmd = ["systemctl", "reload", "named.service"]
logger.info("restarting named.service")
if self._fakesystemd is True:
print(f"here service would be restarted: {cmd}")
else:
subprocess.call(cmd)
Sem muita novidade, é o código pra reiniciar o serviço de dns via systemd.
E aqui acabamos com o código novo. O que achou? Eu achei melhor de manter. E funcionando sem bugs.
O próximo passo será escrever testes unitários pra ele ☺️.
Roubartilhando um vídeo muito bom que a Fabs postou no Mastodon. Explica bastante o fediverso e a importância de estar ali.
Do original:
Só uma pequena nota: estou salvando o vídeo aqui porque as coisas costuma sumir na Internet. Então aqui ficar preservado o vídeo.
Foram inicialmente 10 anos, descritos em 10 anos de Loureiro.Eng.BR.
Depois foram 20 anos, 20 anos de Loureiro.Eng.BR !!!
E chegamos aos 25 anos do site. Pra minha imensa surpresa e prazer pessoal.
Eu esperava chegar nessa marca? Jamais! E continuar escrendo com frequência? Bem, isso eu imaginei sim no início. Aliás achei que era bem mais fácil criar conteúdo do que realmente é.
De notas pessoais a mensagens de blog. Tentei colocar de tudo por aqui. E manter a chama viva. E a chama sobreviveu esse tempo todo. E não parece que vai apagar tão cedo.
Nos vemos daqui 5 anos.
E feliz aniversário pro site.
A Europa tem tentado manter sistemas independentes dos EUA. Não é algo de agora, mas sempre foi um dos pensamentos vigentes por aqui. Quem não lembra do projeto Galileu, pra subistutir o GPS dos americanos caso esse fosse desligado?
E isso tem sido mais acirrado ultimamente com os temas de soberania de dados. Sem falar no Trump. Esse sozinho é um caso à parte.
E uma das iniciativas da Europa é esses sistema de DNS independente de empresas americanas. Eu já adotei aqui em casa.
A EFF, Eletronic Frontier Foundation, lançou uma iniciativa bem legal. Você pode acessar e testar se seu browser mantém seus dados seguros no sentido de privacidade.
Eu testei no meus Firefox, tanto dos computadores (laptops e desktop) tanto quanto no meu telefone. Todos passaram maravilhosamente seguros quanto a minha privacidade.
E você? Já testou seus browser?
Resolvi aproveitar que meu desktop está menos sobrecarregado e rodar os testes novamente pra comparar com os do artigo anterior, revisitando o artigo de shell lento com python3.13.
❯ time python3.13 20M-touch.py; time python3.13t 20M-touch.py
________________________________________________________
Executed in 396.13 secs fish external
usr time 231.65 secs 479.00 micros 231.65 secs
sys time 158.26 secs 803.00 micros 158.26 secs
________________________________________________________
Executed in 22.31 mins fish external
usr time 495.80 secs 0.00 millis 495.80 secs
sys time 820.21 secs 1.22 millis 820.20 secs
De 621 pra 396s já foi um ganho significativo de desempenho. Mas abaixo dos 374s do primeiro artigo, shell é lento?
O python3.13t, quer permite desabilitar o GIL, continua lento. Eu esqueci de desabilitar o GIL e não deveria fazer diferença. Mas fez. 22 minutos. Melhor que os 31 minutos do artigo anterior.
E com GIL desabilitado?
❯ time env PYTHON_GIL=0 python3.13t 20M-touch.py
________________________________________________________
Executed in 22.63 mins fish external
usr time 506.53 secs 381.00 micros 506.53 secs
sys time 822.23 secs 825.00 micros 822.23 secs
Não mudou muita coisa. O jeito é aceitar que é isso e seguir em frente. Segura o choro que dói menos.
Quando escrevi o artigo Shell é lento? python teve uma performance miserável. Pra não chamar de outra coisa.
Resolvi então dar uma revisitada no teste e rodando o python3.13. A cada versão de python dizem que a performance é melhorada. Nada melhor que tirar a prova. E além disso a versão 3.13 permite desabilitar o GIL, o Global Interpreter Locker. Não se se faz alguma diferença num teste desses, mas vamos tentar.
Eu mantive o mesmo programa que rodei da outra vez:
#! /usr/bin/env python3
for i in range(20000000):
with open("arq-python3", "w") as fd:
None
E o resultado:
helio@goosfraba❯ time python3.13 20M-touch.py
________________________________________________________
Executed in 621.80 secs fish external
usr time 384.60 secs 998.00 micros 384.60 secs
sys time 229.78 secs 0.00 micros 229.78 secs
Demorou mais que o teste anterior. Se antes foi miserável, essa aqui... Mas antes de botar a culpa no Python, vamos rodar a versão em Go e olhar se os tempos mudaram. O código de Go também continua o mesmo:
package main
import (
"log"
"os"
)
func main() {
for i := 0; i < 20000000; i++ {
fd, err := os.Create("arq-go")
fd.Close()
if err != nil {
log.Fatal(err)
}
}
}
E depois daquela compilada básica:
helio@goosfraba❯ go build -o 20M-touch 20M-touch.go
helio@goosfraba❯ time ./20M-touch
________________________________________________________
Executed in 295.12 secs fish external
usr time 88.50 secs 0.20 millis 88.50 secs
sys time 199.48 secs 1.03 millis 199.47 secs
Realmente baixou o Exu-tranca-sistema no HDD. Da época em que fiz o primeiro teste pra cá a mudança foi a adição de um disco extra de 12 TB. E afetou bastante a performance. De 148s pra 295s com o binário em Go. Nesse caso é melhor rodar o este com cada linguagem pra ver as diferenças no sistema novo e ter um equilíbrio maior entre os resultados.
helio@goosfraba❯ time perl 20M-touch.pl
________________________________________________________
Executed in 260.95 secs fish external
usr time 61.45 secs 1.31 millis 61.45 secs
sys time 195.20 secs 0.04 millis 195.20 secs
helio@goosfraba❯ time bash 20M-touch.sh
________________________________________________________
Executed in 443.89 secs fish external
usr time 213.20 secs 1.26 millis 213.20 secs
sys time 227.43 secs 0.04 millis 227.43 secs
E pra melhorar o escope de testes, adicionei ainda a versão em C++:
#include <iostream>
#include <fstream>
using namespace std;
int main() {
for (int i=0;i<20000000;i++) {
ofstream MyFile("arq-cpp");
MyFile.close();
}
}
O resultado:
helio@goosfraba❯ time ./20M-touch-cpp
________________________________________________________
Executed in 205.39 secs fish external
usr time 39.56 secs 359.00 micros 39.56 secs
sys time 163.64 secs 911.00 micros 163.64 secs
O resultados foram então (do mais rápido pro mais lento):
Enquanto C++ manteve a performance esperada, perl deu um show. Eu pessoalmente achei que Go! ficou devendo, ainda mais se comparado com C++. Mas python... python fracassou miseravelmente. E de novo.
E fui tentar rodar com o GIL desabilitado e...
helio@goosfraba❯ time env PYTHON_GIL=0 python3.13 20M-touch.py
Fatal Python error: config_read_gil: Disabling the GIL is not supported by this build
Python runtime state: preinitialized
________________________________________________________
Executed in 4.91 millis fish external
usr time 2.03 millis 1.09 millis 0.95 millis
sys time 2.85 millis 0.02 millis 2.83 millis
Vou precisar compilar um python3.13 com a configuração que permite desabilitar o GIL...
Update: compilei um pacote do AUR.
==> Creating package "python313-freethreaded"...
-> Generating .PKGINFO file...
-> Generating .BUILDINFO file...
-> Generating .MTREE file...
-> Compressing package...
==> Leaving fakeroot environment.
==> Finished making: python313-freethreaded 3.13.3-1 (Mon 12 May 2025 06:04:15 PM CEST)
==> Cleaning up...
E vamos aos resultados:
helio@goosfraba❯ time env PYTHON_GIL=0 python3.13t 20M-touch.py
________________________________________________________
Executed in 31.58 mins fish external
usr time 14.01 mins 0.00 millis 14.01 mins
sys time 17.25 mins 1.96 millis 17.25 mins
Python continua parecendo que bateu uma feijuca antes de rodar os testes. Não que remover o GIL fosse mudar muita coisa uma vez que o teste é sequencial. Mas podia ter melhorado um pouco.
Enquanto isso também descobri o porquê dos testes estarem mais lentos: tem algum treco do yay compilando. Sei lá eu o que é uma vez que estou conectado remotamente.
Sempre tenho renovado meus votos sobre o ano do Linux no desktop. E parece que dessa vez deu certo mesmo.
Sem nenhuma ligação com comunidades Linux ou software livre, o influenciador digital "PewDiePie", com milhões de seguidores, simplemente resolveu migrar e enaltecer o uso do Linux. E não pense que foi pouca coisa. O cara já meteu uma variante do archlinux com hyperland.
Simplesmente maravilhoso. Confira:
Se ainda existisse FISL, era certeza que o PewDiePie era o próximo keynote. Quem sabe não seja no FOSDEM?
A querida Bárbara Tostes lançou esse vídeo anunciando seu novo canal e o trabalho que irá fazer por lá. Então aqui está minha contribuição em sua divulgação.
E boa sorte para ela.
Só um comando que uso bastante e provavelmente tem gente que nem sabe que existe.
OpenSSL tem várias funcionalidades, entre elas gerar senhas. O comando que uso é o seguinte:
❯ openssl rand -base64 32 | cut -c 1-43
O resultado é algo como isso aqui (que a cada comando resulta em algo aleatório):
❯ openssl rand -base64 32 | cut -c 1-43
EHjsiO2IIuaBzGmugiVDZgfmj83v/LqZ/nevZ5itzV0
Page 1 of 36