Ir para o conteúdo
ou

Software livre Brasil

Tela cheia
 Feed RSS

Helio Loureiro

27 de Maio de 2009, 0:00 , por Software Livre Brasil - | Ninguém está seguindo este artigo ainda.

Usando expect pra acessar um host no meio do caminho

11 de Junho de 2021, 20:16, por Home - helio.loureiro.eng.br - 0sem comentários ainda

Eu estou no momento trabalhando num troubleshooting de uma rede 5G.  Qual a novidade?  Seria o 5G?  Bom... não.  A diferença é que pra acessar o ambiente cloud eu preciso fazer login numa máquina e depois fazer login em outra máquina.

Nada muito glamouroso, mas não é algo que eu possa escolher não fazer.  Então a forma pra ajudar a ter isso feito da forma mais rápida possível foi através dos uso de expect.

Primeiramente uma visão da rede:

 

Um diagrama feito no libreoffice draw.  Também nada glamouroso, mas deve dar conta do recado.  Eu rodo o script, que pede somente minha senha de rede uma vez que é o mesmo usuário no server-gw1.  E conecta via ssh.  No server-gw2 é o mesmo usuário e senha, o que facilita as coisa.  Dali pra rede k8s não tem mais nada porque eu acesso com um kube.conf e comand kubectl.

O script é esse aqui:


#! /usr/bin/expect

stty -echo
send_user -- "Entre a senha: "
expect_user -re "(.*)\n"
send_user "\n"
stty echo
set PASS $expect_out(1,string)


spawn ssh server-gw1

while {1} {
        expect {
                "ssword:" { send "$PASS\n" }
                "(yes/no)?" { send "yes\n" }
                "\$ " {break}
        }
}

send "ssh server-gw2; exit\n"
while {1} {
        expect {
                "ssword:" { send "$PASS\n" }
                "(yes/no)?" { send "yes\n" }
                "\$ " {break}
        }
}
interact

Note que é possível trocar pra receber o username como input ou como argumento do script.  Na chamada pro segundo servidor existe um 'send "ssh server-gw2; exit\n"'.  O motivo disso é pra quando eu digitar "exit" do servidor server-gw2 não precisar digitar novamente no server-gw1.  Então ele faz o ssh pro server-gw2 e o próximo comando esperando já é um exit.

Espero que ajude e happy coding!



Uma conversa introdutória sobre shell scripts com o prof. Juliano

4 de Junho de 2021, 12:11, por Home - helio.loureiro.eng.br - 0sem comentários ainda

Esses dias, um pouco antes da BSD Day pra dize a verdade, eu aceitei o convite do prof. Juliano pra falar sobre shell scripts.

Eu não preparei muito antes, e fiz um tipo de live coding sobre shell scripts mostrando um pouco de cada coisa.   Não espero que seja tido com um tipo de aprendizado justamente porque eu realmente não preparei nada como aula ou palestra, mas espero que sirva de inspiração pra quem quiser iniciar.

Boa diversão!

Chamada pro vídeo.

 

 

Vídeo completo.

 



Rápida introdução ao Python na BSD Day 2021

30 de Maio de 2021, 13:51, por Home - helio.loureiro.eng.br - 0sem comentários ainda

Não tem sido muito fácil manter o site atualizado com informações semanalmente como eu planejava, mas eu já esperava por isso.  Ao menos tenho escrito com mais frequência que antes.

Um dos motivos é que tenho participados de organização de hackathons (como descrevi uma parte em Rodando desafios de uma hackathon com Python) e ontem foi para palestrar na BSD Day.

Eu não sabia muito bem sobre o que palestrar, então fiz relacionado à programação em Python, em como substituir o que poderia ser feito em shell script por Python.   Foi um live coding, que está apresentado aqui.  Boa diversão!

 



Limpando entradas de um disco removido do LVM

24 de Maio de 2021, 18:22, por Home - helio.loureiro.eng.br - 0sem comentários ainda

E minhas aventuras com LVM continuam.  Um dos HDDs começou a chiar.  Mas chiar mesmo, fazendo barulho de marteladas e resets.   Olhando nos logs eu vi que já tinha dado o que tinha que dar.  E justamente o HDD que fazia parte do mirroring em que estão os jogos da steam.

Então procedi com o comando pra terminar a montagem em mirroring.

lvconvert -m 0 diskspace/steam /dev/sdb3

Infelizmente eu dei um reboot em seguida e não salvei nada pra poder postar aqui.  Mas o mais importante foi depois de removido HDD e instalado um novo (também de 2 TB) que tinha parado aqui.  Como eu esqueci de remover o /dev/sdb3 do LVM, claro que subiu com vários erros. Particionei o novo HDD (/dev/sdb) com uma só partição LVM. E fui adicionar quando...

root@goosfraba ~# pvcreate /dev/sdb1
  WARNING: Couldn't find device with uuid CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa.
  WARNING: VG diskspace is missing PV CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa (last written to /dev/sdb3).
  Physical volume "/dev/sdb1" successfully created.

Epa!  Que catzo de CvlXC4-não-sei-lá-o-que é esse?  Sim, o disco que removi fisicamente e não tirei logicamente do LVM.  Com "vgdisplay" pude ver que realmente o problema estava lá.

root@goosfraba ~# vgdisplay 
  WARNING: Couldn't find device with uuid CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa.
  WARNING: VG diskspace is missing PV CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa (last written to /dev/sdb3).
  --- Volume group ---
  VG Name               diskspace
  System ID             
  Format                lvm2
  Metadata Areas        1
  Metadata Sequence No  230
  VG Access             read/write
  VG Status             resizable
  MAX LV                0
  Cur LV                9
  Open LV               2
  Max PV                0
  Cur PV                2
  Act PV                1
  VG Size               5.45 TiB
  PE Size               4.00 MiB
  Total PE              1429493
  Alloc PE / Size       518912 /

Resolver não foi nada complicado.  Bastou o seguinte comando:

root@goosfraba ~# vgreduce  --removemissing diskspace
  WARNING: Couldn't find device with uuid CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa.
  WARNING: VG diskspace is missing PV CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa (last written to /dev/sdb3).
  WARNING: Couldn't find device with uuid CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa.
  Wrote out consistent volume group diskspace.

Pronto!  Metadados do disco antigo removidos.

root@goosfraba ~# vgdisplay 
  --- Volume group ---
  VG Name               diskspace
  System ID             
  Format                lvm2
  Metadata Areas        1
  Metadata Sequence No  231
  VG Access             read/write
  VG Status             resizable
  MAX LV                0
  Cur LV                9
  Open LV               2
  Max PV                0
  Cur PV                1
  Act PV                1
  VG Size

Agora o trabalho seguinte foi adicionar o espaço novo dentro do VG (diskspace é o nome):

root@goosfraba ~# vgextend diskspace /dev/sdb1
  Volume group "diskspace" successfully extended
root@goosfraba ~# vgdisplay 
  --- Volume group ---
  VG Name               diskspace
  System ID             
  Format                lvm2
  Metadata Areas        2
  Metadata Sequence No  232
  VG Access             read/write
  VG Status             resizable
  MAX LV                0
  Cur LV                9
  Open LV               2
  Max PV                0
  Cur PV                2
  Act PV                2
  VG Size               

Em seguida colocar o disco novo como mirroring novamente:

root@goosfraba ~# lvconvert -m 1 /dev/diskspace/steam /dev/sdb1
Are you sure you want to convert linear LV diskspace/steam to raid1 with 2 images enhancing resilience? [y/n]: y
  Logical volume diskspace/steam successfully converted.

E pronto.  Acabou.  Isso mesmo.  Foi fácil assim.  Agora é só ir monitorando o progresso da cópia no disco novo.

root@goosfraba ~# lvs
  LV       VG        Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  home     diskspace -wi-a----- 500.00g                                                    
  opt      diskspace -wi-a-----   2.00g                                                    
  root     diskspace -wi-ao----  10.00g                                                    
  steam    diskspace rwi-a-r--- 750.00g                                    0.88            
  swap     diskspace -wi-a-----  15.00g                                                    
  tmp      diskspace -wi-a-----   5.00g                                                    
  usr      diskspace -wi-ao----  95.00g                                                    
  usrlocal diskspace -wi-a----- 600.00g                                                    
  var      diskspace -wi-a-----  50.00g                                                    
root@goosfraba ~# lvs
  LV       VG        Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  home     diskspace -wi-a----- 500.00g                                                    
  opt      diskspace -wi-a-----   2.00g                                                    
  root     diskspace -wi-ao----  10.00g                                                    
  steam    diskspace rwi-a-r--- 750.00g                                    1.10            
  swap     diskspace -wi-a-----  15.00g                                                    
  tmp      diskspace -wi-a-----   5.00g                                                    
  usr      diskspace -wi-ao----  95.00g                                                    
  usrlocal diskspace -wi-a----- 600.00g                                                    
  var      diskspace -wi-a-----  50.00g                                                    

Como são 750 GB a coisa demora um pouco.  Mas com LVM, tudo é muito fácil.

Happy hacking :)



Rodando desafios de uma hackathon com Python

8 de Maio de 2021, 16:48, por Home - helio.loureiro.eng.br - 0sem comentários ainda

Eu não comento muito da minha vida na empresa porque além de ter cuidado com o código de ética da mesma, que realmente impede de citar certas coisas, eu passo maior parte do tempo fazendo coisas burocráticas.   Mas desde que mudei da Suécia eu passei a participar dos hackathons internos da empresa.  No início como participante e depois como organizador.   Hoje em dia eu não organizo muita coisa porque a hackthon é toda online.  Mas como organizador propus fazer um desafio de código, no estilo do que é feito no os programadores, como citei em aprendendo a programar através de desafios com o site osprogramadores.

Ontem e hoje fizemos a maratona de código e eu fiquei testando.

Como era uma competição, sem prêmios diga-se de passagem, o formato foi assim:

  1. Os desafios no site wikimedia interno.
  2. Cada participante precisava criar um repositório git, dar acesso ao meu usuário e passar a informação pra mim via chat no ms teams ou por e-mail (tá... foi tosco, mas foi simples e funcionou).
  3. Eu copiava esses repositórios, e monitorava o arquivo Makefile. 
  4. Quando um Makefile surgia com um time stamp de modificação recente, o script rodava um "make all", que era como cada código era construído.  A construção era através de um container chamado devcon, que tem seu Dockerfile num repositório git.  Qualquer módulo, pacote ou o que for pra construir com o make pode ser incluído no container mandando um merge request no Dockerfile.
  5. Esse build deveria gerar um executável chamado "hacking".   Podia ser binário ou script.
  6. Em seguida o código era rodado dentro do ambiente de container do devcon.
  7. Ao final, o tempo total era calculado.  O resultado, verificado por md5.

Bom... vamos começar antes com o container que rodava os desafios, tanto a construção deles, se necessária, quanto sua execução: o container devcon.


FROM ubuntu:18.04

ARG DNS_SERVER
ENV DNS_SERVER ${DNS_SERVER}

ENV DEBIAN_FRONTEND noninteractive
ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE 1

RUN  apt-get -y update \
  && apt-get -y dist-upgrade \
  && apt-get -y install apt-transport-https \
                        apt-utils \
                        ca-certificates \
                        curl \
                        gnupg-agent \
                        software-properties-common \
  && curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" \
  && mv kubectl /usr/local/bin \
  && curl https://get.helm.sh/helm-v2.17.0-linux-amd64.tar.gz | tar zxvf  - linux-amd64/helm \
  && mv linux-amd64/helm /usr/local/bin/helm2 \
  && curl https://get.helm.sh/helm-v3.5.3-linux-amd64.tar.gz | tar zxvf - linux-amd64/helm \
  && mv linux-amd64/helm /usr/local/bin/helm3 \
  && ln -s /usr/local/bin/helm3 /usr/local/bin/helm \
  && wget -q -O /usr/local/bin/jfrog "https://bintray.com/jfrog/jfrog-cli-go/download_file?file_path=1.5.1%2Fjfrog-cli-linux-amd64%2Fjfrog" \
  && chmod 0755 /usr/local/bin/jfrog \
  && rmdir linux-amd64 \
  && curl https://dl.google.com/go/go1.15.7.linux-amd64.tar.gz | tar zxvf - -C /usr/local \
  && ln -s /usr/local/go/bin/* /usr/local/bin \ 
  && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \
  && echo "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable" \
         > /etc/apt/sources.list.d/docker.list \
  && curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
         | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.gpg \
  && echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" \
         > /etc/apt/sources.list.d/vscode.list \
  && curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo "deb https://dl.yarnpkg.com/debian/ stable main" \
         > /etc/apt/sources.list.d/yarn.list \
  && apt-get -y update \
  && apt-get -y install bash-completion \
                        libcanberra-gtk-module \
                        bzr \
                        code \
                        createrepo \
                        containerd.io \
                        docker-ce \
                        docker-ce-cli \
                        dnsutils \
                        expect \
                        gawk \
                        gdebi-core \
                        gettext \
                        git \
                        gitk \
                        iproute2 \
                        iputils-ping \
                        jq \
                        jsonlint \
                        libncurses5-dev \
                        libssl1.0-dev \
                        libterm-ui-perl \
                        libxss1 \
                        lynx \
                        lzip \
                        make \
                        man \
                        meld \
                        mercurial \
                        mc \
                        netcat \
                        net-tools \
                        node-gyp \
                        nodejs-dev \
                        npm \
                        openjdk-8-jdk \
                        openssh-server \
                        pandoc \
                        pkg-config \
                        python \
                        python-pip \
                        python3-pip \
                        python3-setuptools \
                        python3-jinja2 \
                        python3-yaml \
                        rpm \
                        rsyslog \
                        runit \
                        sudo \
                        shellcheck \
                        yarn \
                        vim \
                        vim-scripts \
                        vim-syntax-docker \
                        wget \
                        cpio \
  && apt-get -y install maven \
  && apt-get clean \
  && pip3 install WeasyPrint \
  && sed -i 's/%sudo\tALL=(ALL:ALL)\ ALL/%sudo\tALL=(ALL:ALL) NOPASSWD:ALL/' /etc/sudoers \
  && echo "X11UseLocalhost no" >> /etc/ssh/sshd_config \
  && mkdir /devel; chmod 777 /devel \
  && echo "Europe/Stockholm" > /etc/timezone \
  && dpkg-reconfigure tzdata \
  && mkdir /go \
  && export PATH=/usr/local/go/bin:$PATH \
  && export GOPATH=/go \
  && export GOBIN=/usr/local/bin \
  && go get -v -u github.com/tebeka/go2xunit \
  && go get -v -u golang.org/x/lint/golint \
  && go get -v -u github.com/go-delve/delve/cmd/dlv \
  && go get -v -u github.com/uudashr/gopkgs/v2/cmd/gopkgs \
  && go get -v -u github.com/ramya-rao-a/go-outline \
  && go get -v -u github.com/cweill/gotests/... \
  && go get -v -u github.com/fatih/gomodifytags \
  && go get -v -u github.com/josharian/impl \
  && go get -v -u github.com/haya14busa/goplay/cmd/goplay \
  && GO111MODULE=on go get golang.org/x/tools/gopls@latest \
  && GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0 \
  && go get github.com/securego/gosec/cmd/gosec \
  && rm -rf /go

Eu removi algumas partes de coisas internas, mas é basicamente isso aí o container.   Um ubuntu 18.04 com um go mais recente.

O loop do código era esse aqui, que basicamente entra no diretório de repositórios e busca por diretórios com os nomes "challenge-1", "challenge-2" e "challenge-3".


for directory in get_directories(HACKATHONREPOS):
    full_path = f"{directory}/challenge-" 
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            challenge(i, f"{full_path}{i}", timestamp)

Bem simples.   Mais próximo de um shell script que de um programa.  A variável HACKATHONREPOS apontando pro diretório onde estavam as cópias dos repositórios participantes, get_directories( ) retornando os nomes de diretórios de caminho apontado e challenge( ) pra rodar o teste, send o primeiro passo "make all".   Antes de rodar o programa eu checo a data de modificação de um arquivo de time stamp pra sabe se o programa é mais novo ou mais velho.  Se for mais velho, não preciso rodar.   Então a função get_mtime( ) retorna o tempo em segundos (unix time) da data de modificação do arquivo.  No update_timestamp( ) eu abro o arquivo, ou crio se não existir, e jogo qualquer coisa dentro.  Estou jogando o tempo em segundos com time.time( ), mas realmente não precisa nada.


def update_timestamp():
    with open(TIMESTAMP, 'w') as tmpstamp:
        tmpstamp.write(str(time.time())

def get_mtime(filename):
    return os.stat(filename).st_mtime

if not os.path.exists(TIMESTAMP):
    update_timestamp()
timestamp = get_mtime(TIMESTAMP)

for directory in get_directories(HACKATHONREPOS):
    full_path = f"{directory}/challenge-" 
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            challenge(i, f"{full_path}{i}", timestamp)

update_timestamp()

Pra descrever um pouco mais dos problemas que encontrei, melhor uma olhada mais a fundo na função challenge( ).


def challenge(chl_id, directory, timestamp):
    os.chdir(directory)
    challenge_nr = os.path.basename(os.path.realpath(directory))
    team_name = os.path.basename(os.path.realpath(directory + "/.."))

    print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))

    if os.path.exists(f"{directory}/Makefile"):
        tmstp = get_mtime(f"{directory}/Makefile")
        if tmstp > timestamp:
            try:
                dockerize(directory, "git clean -fdx")
            except subprocess.CalledProcessError:
                pass
            try:
                dockerize(directory, "make all")
            except subprocess.CalledProcessError as e:
                update_results({team_name: {challenge_nr: "failed: to build using make"}})
                print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
                return

    if not os.path.exists(f"{directory}/hacking"):
        print(team_name, challenge_nr, "done - binary missing")
        return

    tmstp = get_mtime(f"{directory}/hacking")
    if timestamp >= tmstp:
        print(team_name, challenge_nr, "done - old timestamp")
        return

    container_name = f"{team_name}_{challenge_nr}"
    print("container_name:", container_name)
    challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
    time_start = time.time()
    try:
        result = dockerize(directory, f"./hacking {challenge_file}", container_name)
    except subprocess.CalledProcessError as e:
        update_results({team_name: { challenge_nr : "failed: to run challenge: "}})
        print(team_name, challenge_nr, "done - failed to run: ", e.output)
        return
    time_stop = time.time()
    print(f"docker ended for {team_name} in {challenge_nr}")

    md5 = md5sum(result)
    print(f"md5 ended for {team_name} in {challenge_nr}")
    if md5 ==  EXPECTED_RESULTS[challenge_nr]:
        update_results({team_name: { challenge_nr : "failed: wrong md5 check"}})
    else:
        update_results({team_name: { challenge_nr : time_stop - time_start}})
    print(team_name, challenge_nr, "done - arrived at the end")

A função challenge está um pouco grande, mas o que ela basicamente faz é olhar se existe um arquivo Makefile com timestamp mais recente e rodar um "make all" pra construir o executável.  O acordo era o binário seria nomeado como "hacking" pra facilitar essa função de rodar.  Ela então busca por hacking na chamada container_name = f"{team_name}_{challenge_nr}" e verifica o timestamp.  Se for mais novo, roda.  Do contrário não faz nada.  Ao rodar os programas é chamada a função dockerize( ), que nada mais é que uma chamada pra docker usando subprocess.  O tempo de início e fim de execução são capturados em time_start e time_stop.  O resutando é verificado com a função md5sum( ), que emula o funcionamento do programa md5sum em Linux.

Bom... já deu pra perceber que o que parecia simples foi ficando bem complicado.   Vamos então dar uma olhada na função dockerize.


def dockerize(pwd, command, container_name=None):
    #userid = os.getuid()
    #groupid = os.getgid()
    userid = 1000
    groupid = 1000
    docker_cmd = [ "docker",
                  "run" ]
    if not container_name is None:
        docker_cmd += [ f"--name={container_name}" ]
    docker_cmd += [
                  "--rm",
                  f"--user={userid}:{groupid}",
                  "-w",
                  f"{pwd}",
                  "-v",
                  f"{pwd}:{pwd}",
                  "-v",
                  f"{ROOTDIR}:{ROOTDIR}",
                  "hackathon:latest" ]

    cmd =  docker_cmd + command.split()
    print("Running:", " ".join(cmd))

    return str(subprocess.check_output(cmd))

 Nada muito sofisticado.  Mas já deu pra ver que buscar o uid e gid deu problemas.  Por quê?  Porque eu deixei rodando numa instância de jenkins, que roda com seu próprio usuário.   Tentei arrumar as permissões do diretório pro mesmo grupo, mas no fim foi mais fácil deixar o container rodar com uid e gid fixos.   Se nunca fez isso em container, experimente.  Funciona que é uma beleza:


docker run --rm --user=$(id -u):$(id -g) -v $PWD:$PWD -w $PWD ubuntu:18.04 ls -a

E isso seria tudo do programa.  Tem a parte do update_results( ), mas vou comentar depois que é somente salvar os resultados no formato json.

O que deu errado?   Muita coisa.

Alguns programas simplesmente travavam.  Ficavam lá parados com algum crash de lib ou coisa do tipo.

Qual foi a solução.  Bom... a solução foi usar threads.  Mas como alguém já disse antes, você tem um problema de dead-lock e quando resolve corrigir usando threads você termina tendo 5 outros problemas.   Mas foi o que fiz.  O loop inicial então foi modificado pra isso aqui:


for directory in get_directories(HACKATHONREPOS):
    full_path = f"{directory}/challenge-" 
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp)).start()
            ths.append(th)
            #challenge(i, f"{full_path}{i}", timestamp)

for th in ths:
    th.join()

update_timestamp()

A diferença era que agora rodavam muita instâncias ao mesmo tempo, tudo em paralelo, e acabava usando CPU demais.  Então pra resolver um problema, criei outro.  Precisei criar um semáforo pra poder dizer quantos threads poderia rodar simultaneamentes.  Lembram do update_results( ) que salvava em json?  O que acontece quando várias threads tentam escrever no mesmo arquivo ao mesmo tempo?  Ou você chega numa estado chamado race condition, ou simplesmente dados aparecem e somem.  Então foi preciso criar dois semáforos: um pro número de threads e outro pra salvar o resultado.

O loop principal então ficou assim:


ths = []
q = threading.Semaphore(MAX_THREADS)
qs = threading.Semaphore(1)
for directory in get_directories(HACKATHONREPOS):
    #print(directory)
    full_path = f"{directory}/challenge-" 
    #print(full_path)
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp, q, qs)).start()
            ths.append(th)
            #challenge(i, f"{full_path}{i}", timestamp, q, qs)

for th in ths:
    th.join()

update_timestamp()

E já que dois novos parâmetros foram passados pra função challenge( ), como essa ficou internamente?  Assim:


def challenge(chl_id, directory, timestamp, q, qs):
    q.acquire()
    print(chl_id, directory, timestamp)
    os.chdir(directory)
    challenge_nr = os.path.basename(os.path.realpath(directory))
    team_name = os.path.basename(os.path.realpath(directory + "/.."))

    print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))

    if os.path.exists(f"{directory}/Makefile"):
        tmstp = get_mtime(f"{directory}/Makefile")
        if tmstp > timestamp:
            try:
                dockerize(directory, "git clean -fdx")
            except subprocess.CalledProcessError:
                pass
            try:
                dockerize(directory, "make all")
            except subprocess.CalledProcessError as e:
                update_results({team_name: {challenge_nr: "failed: to build using make"} + str(e.output)}, qs)
                print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
                q.release()
                return

    if not os.path.exists(f"{directory}/hacking"):
        print(team_name, challenge_nr, "done - binary missing")
        print(os.listdir("."))
        q.release()
        return
    tmstp = get_mtime(f"{directory}/hacking")
    if timestamp >= tmstp:
        print(team_name, challenge_nr, "done - old timestamp")
        q.release()
        return

    container_name = f"{team_name}_{challenge_nr}"
    print("container_name:", container_name)
    challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
    time_start = time.time()
    try:
        result = dockerize(directory, f"./hacking {challenge_file}", container_name)
    except subprocess.CalledProcessError as e:
        update_results({team_name: { challenge_nr : "failed: to run challenge: " + str(e.output)}}, qs)
        print(team_name, challenge_nr, "done - failed to run: ", e.output)
        q.release()
        return
    time_stop = time.time()
    print(f"docker ended for {team_name} in {challenge_nr}")

    md5 = md5sum(result)
    print(f"md5 ended for {team_name} in {challenge_nr}")
    if md5 ==  EXPECTED_RESULTS[challenge_nr]:
        update_results({team_name: { challenge_nr : "failed: wrong md5 check"}}, qs)
    else:
        update_results({team_name: { challenge_nr : time_stop - time_start}}, qs)
    print(team_name, challenge_nr, "done - arrived at the end")
    q.release()

Basicamente um q.acquire( ) pra começar a rodar e um q.release( ) ao terminar.  Tudo lindo.  Vamos rodar?  Via Jenkinks claro?  E... problemas.   Algumas dessas threads ficavam paradas.  Caso não tenha reparado, ao final do loop principal existe esse pequeno trecho de código:

for th in ths:
    th.join()

 Ele basica diz o seguinte: pra cada thread nesse vetor de threads, espere a thread terminar.  É isso que o join( ) faz.  E como os programas travavam em sua execução o que acontecia?  Dead-lock de novo.

O que fazer então?  Vamos criar um sistema de monitoração chamado... soulkiller!   Se jogou Cybepunk 2077 sabe do que estou falando, certo Silverhand?  Então o soulkiller fica aguardando um certo tempo pra terminar a execução.  Se passar daqui, uma vez sendo container basta simplesmente chamar um "docker kill <nome do container>".  E aliás esse foi o motivo de eu passar o nome do container como argumento da função docker( ).


def soulkiller(container_name, timeout=None):
    print("soulkiller has started for:", container_name)
    time.sleep(3)
    if timeout is None:
        timeout = TIMEOUT
    timeout -= 3
    while timeout > 0:
        resp = exec("docker ps -a")
        if not re.search(container_name, resp):
            print("soulkiller exiting since no container found for:", container_name)
            return
        timeout -= 1
        time.sleep(1)
    print("soulkiller reached timeout and will kill:", container_name)
    exec(f"docker kill {container_name}")

E assim o sistema funcionou durante a hackathon.   Era pra ser simples mas... bom... funcionou.  Abaixo segue o script inteiro em todo sua beleza.  Ou não.


#! /usr/bin/python3


import json
import os
import subprocess
import time
import hashlib
import threading
import queue
import re


MAX_THREADS = 1
TIMEOUT = 30 * 60 * 60

CHALLENGE_INPUTS = {
    "challenge-1": "Employees-30M.json",
    "challenge-2": "1GB.txt",
    "challenge-3": "pi-1M.txt"
  }
 

EXPECTED_RESULTS = {
    "challenge-1": "d5c140cdc965be8ed56c35f570eaf83f",
    "challenge-2": "2b4fd25f11d75c285ec69ecac420bd07",
    "challenge-3": "731fa54d7133f61d4b3fac9b46bda927"
}


ROOTDIR = "/usr/local/tmp/hackathon"
TIMESTAMP = ROOTDIR + "/timestamp"
HACKATHONREPOS = ROOTDIR + "/repos"
RESULTS = ROOTDIR + "/results.json"


def get_directories(dir_name):
    #print("dir_name:", dir_name)
    directories = []
    for filename in os.listdir(dir_name):
        if filename[0] == ".":
            #print(filename, " begins with a dot")
            continue
        filename = f"{dir_name}/{filename}"
        if not os.path.isdir(filename):
            #print(filename, " isn't a directory")
            continue
        #print("appending:", filename)
        directories.append(filename)
    return directories


def exec(command):
    return str(subprocess.check_output(command.split()))


def dockerize(pwd, command, container_name=None):
    #userid = os.getuid()
    #groupid = os.getgid()
    userid = 1000
    groupid = 1000
    docker_cmd = [ "docker",
                  "run" ]
    if not container_name is None:
        docker_cmd += [ f"--name={container_name}" ]
    docker_cmd += [
                  "--rm",
                  f"--user={userid}:{groupid}",
                  "-w",
                  f"{pwd}",
                  "-v",
                  f"{pwd}:{pwd}",
                  "-v",
                  f"{ROOTDIR}:{ROOTDIR}",
                  "hackathon:latest" ]

    cmd =  docker_cmd + command.split()
    print("Running:", " ".join(cmd))

    return str(subprocess.check_output(cmd))


def update_timestamp():
    with open(TIMESTAMP, 'w') as tmpstamp:
        tmpstamp.write(str(time.time()))


def get_mtime(filename):
    return os.stat(filename).st_mtime


def read_results():
    j = json.loads("{}")
    if os.path.exists(RESULTS):
        #print(RESULTS, "exists")
        with open(RESULTS) as results:
            j = json.load(results)
    return j


def save_results(j):
    with open(RESULTS, "w") as output:
        json.dump(j, output, indent=4)


def update_results(response_dict, qs):
    print("Called update_result:", response_dict)
    qs.acquire()
    j = read_results()
    print("Before:", j)
    for team_name_resp in response_dict.keys():
        for challenge_id_resp in response_dict[team_name_resp].keys():
            value_resp = response_dict[team_name_resp][challenge_id_resp]
            if not team_name_resp in j.keys():
                j[team_name_resp] = { challenge_id_resp: value_resp }
            elif not challenge_id_resp in j[team_name_resp].keys():
                j[team_name_resp][challenge_id_resp] = value_resp
            else:
                if not challenge_id_resp in j[team_name_resp].keys():
                    j[team_name_resp][challenge_id_resp] = value_resp
                else:
                    previous_value = j[team_name_resp][challenge_id_resp]
                    if isinstance(previous_value, float) and isinstance(value_resp, float):
                        if value_resp < previous_value:
                            j[team_name_resp][challenge_id_resp] = value_resp
                    else:
                        j[team_name_resp][challenge_id_resp] = value_resp
    print("After:", j)
    save_results(j)
    qs.release()


def md5sum(message):
    hash = hashlib.md5(message.encode())
    return hash.digest()


def soulkiller(container_name, timeout=None):
    print("soulkiller has started for:", container_name)
    time.sleep(3)
    if timeout is None:
        timeout = TIMEOUT
    timeout -= 3
    while timeout > 0:
        resp = exec("docker ps -a")
        if not re.search(container_name, resp):
            print("soulkiller exiting since no container found for:", container_name)
            return
        timeout -= 1
        time.sleep(1)
    print("soulkiller reached timeout and will kill:", container_name)
    exec(f"docker kill {container_name}")


def challenge(chl_id, directory, timestamp, q, qs):
    q.acquire()
    print(chl_id, directory, timestamp)
    os.chdir(directory)
    challenge_nr = os.path.basename(os.path.realpath(directory))
    team_name = os.path.basename(os.path.realpath(directory + "/.."))

    print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))

    if os.path.exists(f"{directory}/Makefile"):
        tmstp = get_mtime(f"{directory}/Makefile")
        if tmstp > timestamp:
            try:
                dockerize(directory, "git clean -fdx")
            except subprocess.CalledProcessError:
                pass
            try:
                dockerize(directory, "make all")
            except subprocess.CalledProcessError as e:
                update_results({team_name: {challenge_nr: "failed: to build using make"}}, qs)
                print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
                q.release()
                return

    if not os.path.exists(f"{directory}/hacking"):
        print(team_name, challenge_nr, "done - binary missing")
        print(os.listdir("."))
        q.release()
        return
    tmstp = get_mtime(f"{directory}/hacking")
    if timestamp >= tmstp:
        print(team_name, challenge_nr, "done - old timestamp")
        q.release()
        return

    container_name = f"{team_name}_{challenge_nr}"
    print("container_name:", container_name)
    threading.Thread(target=soulkiller, args=(container_name,), daemon=True).start()
    challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
    time_start = time.time()
    try:
        result = dockerize(directory, f"./hacking {challenge_file}", container_name)
    except subprocess.CalledProcessError as e:
        update_results({team_name: { challenge_nr : "failed: to run challenge: " + str(e.output)}}, qs)
        print(team_name, challenge_nr, "done - failed to run: ", e.output)
        q.release()
        return
    time_stop = time.time()
    print(f"docker ended for {team_name} in {challenge_nr}")

    md5 = md5sum(result)
    print(f"md5 ended for {team_name} in {challenge_nr}")
    if md5 ==  EXPECTED_RESULTS[challenge_nr]:
        update_results({team_name: { challenge_nr : "failed: wrong md5 check"}}, qs)
    else:
        update_results({team_name: { challenge_nr : time_stop - time_start}}, qs)
    print(team_name, challenge_nr, "done - arrived at the end")
    q.release()

if not os.path.exists(TIMESTAMP):
    update_timestamp()
timestamp = get_mtime(TIMESTAMP)

ths = []
q = threading.Semaphore(MAX_THREADS)
qs = threading.Semaphore(1)
for directory in get_directories(HACKATHONREPOS):
    full_path = f"{directory}/challenge-" 
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp, q, qs)).start()
            ths.append(th)
            #challenge(i, f"{full_path}{i}", timestamp, q, qs)

for th in ths:
    try:
        th.join()
    except:
        pass

update_timestamp()

Se estiver lendo com atenção notará com que rodei com... 1 thread só.  Como eram desafios que exigiam computação e o parâmetro era tempo pra decidir o vencedor, no fim eu decidi deixar uma thread só pra ser justo.



Tags deste artigo: #debian #debianbr #debianse #softwarelivre #freesoftware #linux #python