NetDevOps - Construindo um Pipeline de Integração Contínua (CI)

24 minute read

alt

Fala NetCoders… Neste artigo iremos dar continuidade nos temas que envolvem NetDevOps. Hoje de fato, iremos botar a mão na massa e implementar nosso pipeline de Integração Continua.

Iremos desmistificar o conceito por trás de Network Infrastructure As Code e montaremos nosso pipeline e integrar diversos serviços para montarmos uma estrutura poderosa de automação.

Hoje iremos mexer com Vrnetlab para lançar nosso ambiente de teste automatizado de Rede, esta ferramenta irá lançar as imagens via Docker contêiner.

Além do Vrnetlab e Docker, neste artigo você aprenderá a trabalhar com Git, GitHub e GitHub Actions.

Utilizaremos comandos Git para manipularmos nosso repositório local e remoto, iremos utilizar o GitHub para lançar nossa topologia baseada em código por meio deste repositório, e o GitHub Actions para montarmos nossa estrutura de build. Essa estrutura sempre ficará “buildavel” para que sejam entregues versões da esteira de testes contínuos.

Pegue seu café e vamos nessa!

Tópicos

  • Por que Network Infrastructure As Code?
  • Como funciona o Vrnetlab?
  • Instalando o KVM
  • Instalando o Docker
  • Repositório clone Vrnetlab
  • Construindo imagens com dispositivos virtuais
  • Lançando dispositivos virtuais
  • Funções bash auxiliares
  • Configurando enlace entre contêineres
  • Docker Compose
  • Inicializando o GitHub Action
  • Comandos Git básicos
  • GitHub Runner auto-hospedado
  • Execução de ação GitHub
  • Recapitulação e resumo

Por que Network Infrastructure As Code?

Uma das tarefas mais importantes no desenvolvimento de software é o teste. E se pensarmos que a automação e programabilidade da rede também são um tipo de desenvolvimento de software, também precisamos do teste de rede.

Com uma plataforma de teste de rede sólida, podemos testar o código Ansible (yaml), código Python, netconf/restconf, modelos yang, etc.

Claro que também podemos testar CLI clássico e configurar laboratórios MPLS / EVPN / etc manualmente. Resumindo, um laboratório de teste de rede é importante.

Um resultado importante que podemos obter com uma plataforma de teste de rede é a iterabilidade. Com iterabilidade testamos quantas vezes precisarmos de uma forma muito fácil e automática.

Podemos testar uma implementação, verificar se funciona ou não, se não funcionar lançamos outro laboratório de teste e testamos novamente. Este loop de iteração é muito importante em qualquer processo de desenvolvimento de software.

Recapitulando, neste artigo irá ser construído um pipeline de integração continua onde será usado o Docker para executar duas imagens contêineres EOS Arista. Iremos fazer o deploy de duas caixas virtuais e integra-las à um servidor Github utilizando o serviço de integração continua Github Action.

Como funciona o Vrnetlab?

Esta ferramenta permite executar routers virtuais dentro de contêineres Docker. Ótima ferramenta para montar ambientes de testes vinculados ao GitHub, onde é possível implementar pipelines de teste (CI) e implantação (CD).

Ao construir sua esteira CI, nenhum ser humano é necessário para ativar o ambiente e executar os testes ou validações necessárias.

Graças à e ferramenta, podemos facilmente emular diferentes roteadores e de diferentes fornecedores (Cisco, Juniper, Arista, Nokia, etc) como contêineres Docker, todos eles rodando na própria máquina local.

Vrnetlab é muito leve, é focado apenas em laboratórios pequenos com foco em pipelines automatizados. Você não instala nenhum tipo de software para emular as imagens dos routers, você utiliza ela através do docker.

Quem é responsável por emular de fato a imagem do router seria o KVM, ferramenta de virtualização, a imagem que é executada é do tipo Qemu.

Instalando KVM

Primeiro, certifique-se de que a virtualização está habilitada, “0” significa que não há suporte para virtualização no HW, “1” ou mais, é sinal que esta OK:

thiago@thiago:~$ egrep --count "vmx|smv" /proc/cpuinfo

Instalando as dependências de pacotes de requisitos do KVM:

thiago@thiago:~$ sudo apt-get install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils

Adicione seu usuário aos grupos ‘“kvm” e “libvirt”. Faça logout e login depois para que as alterações tenham efeito:

thiago@thiago:~$ sudo adduser `id -un` kvm
Adicionando o usuário `thiago' ao grupo `kvm' ...
Adicionando usuário thiago ao grupo kvm
Concluído.

thiago@thiago:~$ sudo adduser `id -un` libvirt
O usuário 'thiago' já é um membro de 'libvirt'.

O comando descrito abaixo é para confirmar se o KVM está instalado e operacional. A saída vazia está OK, se algo deu errado, você obterá erros:

thiago@thiago:~$ virsh list --all
 Id    Name                           State
----------------------------------------------------

Instalando o Docker

Para saber os passos para instalação do Docker, fiz um post no qual explica os detalhes da instalação desta ferramenta. Você pode visualizar o seguinte artigo clicando aqui.

Repositório clone Vrnetlab

A maneira mais rápida de obter o Vrnetlab é cloná-lo de seu repositório:

thiago@thiago:~$ mkdir netdev 
thiago@thiago:~$ cd netdev/
thiago@thiago:~/netdev$ git clone https://github.com/plajjan/vrnetlab.git

E é isso! Vrnetlab e todas as suas dependências estão instaladas e estamos prontos para começar!

Construindo imagens com dispositivos virtuais

Mencionei anteriormente que o Vrnetlab fornece scripts de construção para muitos roteadores virtuais de diferentes fornecedores. O que não conseguimos, entretanto, são as imagens reais.

As licenças que vêm com os aparelhos não permitem o reempacotamento e distribuição de outras fontes além dos canais oficiais. Teremos que obter imagens nós mesmos.

Se você não tem nenhuma imagem e deseja apenas acompanhar este post, você pode se cadastrar gratuitamente na Arista e baixar a imagem neste link:

Você precisará de duas imagens:

  • Aboot-veos-serial-8.0.0.iso
  • vEOS-lab-4.18.10M.vmdk

Elas estão destacadas em vermelho na captura de tela abaixo: alt

Depois de fazer o download das imagens, você deverá copiá-las para o diretório veos dentro do diretório vrnetlab. O resultado final deve corresponder à saída abaixo:

thiago@thiago:~/netdev/vrnetlab/veos$ ls
Aboot-veos-serial-8.0.0.iso  docker  Makefile  README.md  vEOS-lab-4.18.10M.vmdk

Com os arquivos no lugar, estamos prontos para iniciar a construção da imagem Docker executando o make comando dentro do diretório:

thiago@thiago:~/netdev/vrnetlab/veos$ make

Se tudo funcionou corretamente, agora você deve ter uma nova imagem Docker disponível localmente. Você pode confirmar isso executando o comando docker images:

thiago@thiago:~/netdev/vrnetlab/veos$ docker images vrnetlab/vr-veos
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
vrnetlab/vr-veos    4.18.10M            12915790e700        About a minute ago   889MB

Opcionalmente, você pode renomear a imagem, para torná-lo mais curto ou se quiser enviá-la para o registro do Docker local:

thiago@thiago:~/netdev/vrnetlab/veos$ docker tag vrnetlab/vr-veos:4.18.10M veos:4.18.10M

Agora nossa imagem tem 02 nomes diferentes:

thiago@thiago:~/netdev/vrnetlab/veos$ docker images | grep 4.18.10
veos                        4.18.10M            12915790e700        5 minutes ago       889MB
vrnetlab/vr-veos            4.18.10M            12915790e700        5 minutes ago       889MB

Você pode excluir com segurança o nome padrão se desejar:

thiago@thiago:~/netdev/vrnetlab/veos$ docker rmi vrnetlab/vr-veos:4.18.10M
Untagged: vrnetlab/vr-veos:4.18.10M
thiago@thiago:~/netdev/vrnetlab/veos$
thiago@thiago:~/netdev/vrnetlab/veos$ docker images | grep 4.18.10
veos                        4.18.10M            12915790e700        7 minutes ago       889MB

Lançando dispositivos virtuais

Até aqui, todas as peças estão no lugar para que possamos executar nosso primeiro router virtual no Docker!

O comando docker run -d --name veos1 --privileged veos:4.18.10M executa o contêiner.

Isso diz ao Docker para iniciar um novo contêiner em segundo plano usando a imagem que construímos. O argumento --privileged é exigido pelo KVM, o argumento --name fornece nosso nome escolhido para o contêiner e o último argumento veos:4.18.10M é o nome da imagem.

Aqui está o comando em ação:

thiago@thiago:~/netdev/vrnetlab/veos$ docker run -d --name veos1 --privileged veos:4.18.10M
1a83898943fd3f16cb132868994cce4906b9ec1bacb5d18694a409ab24ce3eb7
thiago@thiago:~/netdev/vrnetlab/veos$ 
thiago@thiago:~/netdev/vrnetlab/veos$ docker ps | grep veos1
1a83898943fd        veos:4.18.10M       "/launch.py"        16 seconds ago      Up 16 seconds (health: starting)   22/tcp, 80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp   veos1

Se tudo funcionar, devemos obter imediatamente o ID do contêiner. Depois disso, podemos verificar se o contêiner está rodando com o comando  docker ps.

No nosso caso, o contêiner está ativo, mas ainda não está pronto, isso ocorre porque temos (health: starting) na saída do comando  docker ps

Vrnetlab cria imagens Docker com um script de verificação de integridade que nos permite verificar se o contêiner está totalmente ativo e pronto para a ação.

Com o vEOS, geralmente leva cerca de 3 minutos para que o contêiner esteja totalmente estável:

thiago@thiago-ThinkPad-T430:~/netdev/vrnetlab/veos$ docker ps | grep veos1
1a83898943fd        veos:4.18.10M       "/launch.py"        2 minutes ago       Up 2 minutes (unhealthy)   22/tcp, 80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp   veos1

Aqui vamos nós, podemos ver que nosso dispositivo virtual está totalmente instalado e totalmente estável. Então o que vem depois? Provavelmente deveríamos entrar no dispositivo e brincar, certo?

Espere, mas como fazemos isso? Leia mais para descobrir!

Funções bash auxiliares

Vrnetlab vem com algumas funções úteis definidas no arquivo [vrnetlab.sh](http://vrnetlab.sh). Se você usar bash como shell, poderá carregá-los com o comando . vrnetlab.sh ou source vrnetlab.sh. Se isso não funcionar, você terá que consultar o manual do seu shell:

thiago@thiago:~/netdev/vrnetlab$ ls | grep vrnet
. vrnetlab.sh
thiago@thiago:~/netdev/vrnetlab$ . vrnetlab.sh

Uma vez que seu shell carregou as funções, você deve ter acesso ao comando vrcons CONTAINER_NAME - Conecta-se à console do seu dispositivo virtual.

thiago@thiago-ThinkPad-T430:~/netdev/vrnetlab$ docker ps 
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS                                                                  NAMES
1a83898943fd        veos:4.18.10M       "/launch.py"        27 minutes ago      Up 27 minutes (healthy)   22/tcp, 80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp   veos1
thiago@thiago:~/netdev/vrnetlab$
thiago@thiago:~/netdev/vrnetlab$ vr_mgmt_ip veos1
172.17.0.2
thiago@thiago:~/netdev/vrnetlab$
thiago@thiago:~/netdev/vrnetlab$ vrcons veos1
Trying 172.17.0.2...
Connected to 172.17.0.2.
Escape character is '^]'.

localhost#

ssh CONTAINER_NAME [USERNAME] - loga no dispositivo via ssh. Se USERNAME não for fornecido, o nome de usuário é admin e será usado com a senha padrão sendo VR-netlab9.

Identificando usuários dentro do router:

localhost# who
    Line      User        Host(s)       Idle        Location 
*  1 con 0    admin       idle          00:00:42    -

Bom, olhem isso, estamos dentro e tudo parece estar em ordem!

Costumo fazer exatamente isso, iniciar um contêiner, testar alguns comandos e pronto. É surreal a rapidez com que você pode rodar um dispositivo de teste e se livrar dele quando terminar. 

Quase nada está envolvido na configuração. Você só precisa esperar alguns minutos e está tudo pronto para você.

Ótimo, construímos um contêiner com uma imagem vEOS, conseguimos executar alguns comandos. Mas, embora isso possa ser ótimo para criar rapidamente alguns comandos, queremos mais, queremos conectar dispositivos virtuais entre si.

Configurando enlace entre contêineres

Antes de qualquer coisa, vamos preparar o segundo contêiner para que esteja pronto quando precisarmos:

thiago@thiago:~/netdev/vrnetlab$ docker run -d --name veos2 --privileged veos:4.18.10M
b056ba58096647f882885421dd7b68d287c68d7993906a0fdeff21191222415f

Depois iremos verificar o endereço da interface de mngt dos dois routers, vale ressaltar que as duas interfaces de mngt podem estar na mesma rede, não tem problema:

thiago@thiago:~/netdev/vrnetlab$ vr_mgmt_ip veos1
172.17.0.2
thiago@thiagothiago@thiago-ThinkPad-T430:~/netdev/vrnetlab$ vr_mgmt_ip veos2
172.17.0.3

Para criar conexões entre dispositivos, precisamos construir uma imagem Docker especial chamada vr-xcon. Os contêineres que usam esta imagem fornecerão conectividade entre nossas rotas virtuais.

Para construir esta imagem, navegue até o vr-xcon. Assim que estiver no diretório, inserir o comando make. Os scripts dentro das dependências do Vrnetlab farão o resto.

thiago@thiago:~/netdev/vrnetlab/vr-xcon$ ls
ci-builder-image    CONTRIBUTING.md  Makefile                  nxos       sros              vqfx         vr-xcon
CODE_OF_CONDUCT.md  csr              makefile.include          openwrt    topology-machine  vr-bgp       vsr1000
common              git-lfs-repo.sh  makefile-install.include  README.md  veos              vrnetlab.sh  xrv
config-engine-lite  LICENSE          makefile-sanity.include   routeros   vmx               vrp          xrv9k
thiago@thiago:~/netdev/vrnetlab/vr-xcon$
thiago@thiago:~/netdev/vrnetlab/vr-xcon$ make

E para confirmar que a imagem já está disponível:

thiago@thiago:~/netdev/vrnetlab/vr-xcon$:~/netdev/vrnetlab/vr-xcon$ docker images | grep xcon
vrnetlab/vr-xcon            latest              9d2298bb92b0        4 minutes ago       147MB

Precisamos apenas mudar seu nome vr-xcon para fazê-lo funcionar com scripts Vrnetlab.

thiago@thiago:~/netdev/vrnetlab/vr-xcon$ docker tag vrnetlab/vr-xcon:latest vr-xcon
thiago@thiago:~/netdev/vrnetlab/vr-xcon$ 
thiago@thiago:~/netdev/vrnetlab/vr-xcon$ docker images | grep "^vr-xcon"
vr-xcon                     latest              9d2298bb92b0        6 minutes ago       147MB

Em seguida, iremos configurar o enlace virtual entre o contêiner veos1 e veos2:

thiago@thiago:~/netdev/vrnetlab$ cd vr-xcon/
thiago@thiago:~/netdev/vrnetlab/vr-xcon$ docker run -d --name vr-xcon1 --link veos1 --link veos2 vr-xcon --p2p veos1/2--veos2/2
d8c3399e7b6c946cb47f12f78776d40adf1e9a4b0641ad8cfd43c64a3dcd1548

Abaixo está o detalhamento do comando:

  • docker run -d
    • Isso diz ao Docker para executar o contêiner em segundo plano.
  • --name vr-xcon1
    • Queremos que nosso contêiner tenha um nome, você pode chamá-lo de algo diferente, se quiser.
  • --link veos1 --link veos2vr-xconveos1veos2
    • Aqui, dizemos ao Docker para se conectar a contêineres e, isso permite que eles descubram e conversem entre si. vr-xcon1 A segunda referência vr-xconé o nome da imagem a ser executada.
  • --p2p veos1/2--veos2/2
    • Finalmente, temos argumentos que são passados para o contêiner. Aqui, pedimos uma conexão ponto a ponto entre a porta 2 do contêiner veos1 e também a porta 2 do contêiner veos2.
    • A porta 1 é mapeada para a porta de gerenciamento, de forma que a porta 2 será Ethernet1 dentro do roteador virtual.

Parece promissor, o contêiner está ativo e os logs estão vazios, portanto, nenhum erro foi relatado.

thiago@thiago:~/netdev/vrnetlab$ docker logs vr-xcon

Espero que agora você possa ver como tudo se encaixa. Para confirmar se o contêiner está instalado e funcionando, executaremos o comando docker ps para verificarmos os registros do contêiner:

thiago@thiago:~/netdev/vrnetlab$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS                                                                  NAMES
d8c3399e7b6c        vr-xcon             "/xcon.py --p2p veos…"   22 minutes ago      Up 22 minutes                                                                                    vr-xcon1
75c2ffdec332        veos:4.18.10M       "/launch.py"             26 minutes ago      Up 26 minutes (healthy)   22/tcp, 80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp   veos2
b056ba580966        veos:4.18.10M       "/launch.py"             27 minutes ago      Up 27 minutes (healthy)   22/tcp, 80/tcp, 443/tcp, 830/tcp, 5000/tcp, 10000-10099/tcp, 161/udp   veos1

E os mesmos links criados usando três instâncias de vr-xcon contêiner. Caso temos mais roteadores, podemos rodar diversas instâncias de contêiner interligando essas instâncias em p2p. Segue o formato como exemplo:

docker run -d --name vr-xcon1 --link veos1 --link veos2 vr-xcon --p2p veos1/2--veos2/2
docker run -d --name vr-xcon2 --link veos1 --link veos3 vr-xcon --p2p veos1/10--veos3/10
docker run -d --name vr-xcon3 --link veos2 --link veos3 vr-xcon --p2p veos2/5-veos3/5

Como estamos rodando 02 routers virtuais dentro de 02 contêineres, iremos deixar apenas a instância vr-xcon1 rodando, está instância conecta o router veos1 e veos2.

Pronto, enlace entre os contêineres já estão configurados e estamos prontos para fazer login nos dispositivos e confirmar se nossa conexão recém-criada está ativa.

Vamos configurar os nomes de host para que a saída da verificação LLDP os inclua. Em seguida, iremos configurar os IPs e tentaremos fazer o ping entre os routers.

Iremos logar nos dois routers e alterar o hostname:

thiago@thiago:~/netdev/vrnetlab$ vrcons veos1
Trying 172.17.0.2...
Connected to 172.17.0.2.
Escape character is '^]'.

localhost#conf t
localhost(config)#host veos1
thiago@thiago:~/netdev/vrnetlab$ vrcons veos2
Trying 172.17.0.3...
Connected to 172.17.0.3.
Escape character is '^]'.

localhost#en
localhost#conf t
localhost(config)#host veos2

Agora iremos indexar um endereço IP na interface eth1 nos dois routers e colocando-os na mesma rede para que os dois routers virtuais possam se comunicarem:

veos1(config)#int eth1
veos1(config-if-Et1)#no switchport 
veos1(config-if-Et1)#ip address 10.0.1.1/30
veos1(config-if-Et1)#end
veos1#
veos2(config)#int eth1
veos2(config-if-Et1)#no switchport 
veos2(config-if-Et1)#ip address 10.0.1.2/30
veos2(config-if-Et1)#end
veos2#
veos2#sh lldp neighbors 
Last table change time   : 0:03:36 ago
Number of table inserts  : 1
Number of table deletes  : 0
Number of table drops    : 0
Number of table age-outs : 0

Port       Neighbor Device ID               Neighbor Port ID           TTL
Et1        veos1                            Ethernet1                  120
veos2#
veos2#ping 10.0.1.1
PING 10.0.1.1 (10.0.1.1) 72(100) bytes of data.
80 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=643 ms
80 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=209 ms
80 bytes from 10.0.1.1: icmp_seq=3 ttl=64 time=422 ms
80 bytes from 10.0.1.1: icmp_seq=4 ttl=64 time=319 ms
80 bytes from 10.0.1.1: icmp_seq=5 ttl=64 time=145 ms

--- 10.0.1.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 2341ms
rtt min/avg/max/mdev = 145.867/348.204/643.610/175.346 ms, ipg/ewma 585.485/488.423 ms
veos2#

Pronto, conseguimos instalar as imagens no Vrnetlab via Docker, conseguimos subir estas imagens no Docker e implantamos dois routers virtuais e executamos uma instância baseado apenas no link entre eles para que eles consigam se conectarem virtualmente.

Dessa forma, montamos nosso ambiente de lab virtual onde possamos rodar nossos pipelines de testes automatizados.

Finalmente, para remover links, iremos remover o contêiner usado para criar conexões:

thiago@thiago:~/netdev/vrnetlab$ docker rm -f vr-xcon1
vr-xcon1

Depois de terminar o laboratório, caso queira se livrar dos contêineres, você deve executar o comando docker rm -f:

thiago@thiago:~/netdev/vrnetlab$ docker rm -f veos1 veos2

E com isso seus contêineres irão embora.

Docker Compose

Agora você deve saber como trazer os contêineres manualmente e como conectá-los. Pode ser um pouco enfadonho, porém, se você costuma usar a mesma topologia para teste. Por que não escrever algum tipo de receita de laboratório que possamos lançar com um único comando? 

Bem, podemos fazer isso e faremos!

Existem muitas maneiras de fazer isso e, para nosso exemplo, usaremos o Docker Compose. Vamos criar um docker-compose.yml arquivo que trará duas imagens virtuais e as conectam. Como bônus.

Dica: Se você não instalou o Docker Compose, você pode obtê-lo executando sudo apt install docker-compose no Ubuntu. Outras distros também devem tê-lo disponível.

O Compose usa o docker-compose.yml arquivo para definir os serviços a serem executados juntos.

Eu escrevi um desses arquivos que trará 2 dispositivos vEOS com um link entre eles:

thiago@thiago:~/netdev/dcompose/veos-lab$ cat compose.yml 
---

version: "3"

services:
  veos1:
    image: veos:4.18.10M
    container_name: veos1
    privileged: true
    ports:
      - "9001:22"
    network_mode: default
  veos2:
    image: veos:4.18.10M
    container_name: veos2
    privileged: true
    ports:
      - "9002:22"
    network_mode: default
  vr-xcon:
    image: vr-xcon
    container_name: vr-xcon1
    links:
      - veos1
      - veos2
    command: --p2p veos1/2--veos2/2
    depends_on:
      - veos1
      - veos2
    network_mode: default

Se for a primeira vez que você vê docker-compose.yml, não se preocupe, estou dividindo para você abaixo.

  • A primeira linha define a versão do formato Compose, version: "3" é bastante antigo, mas é amplamente suportado.
  • Na seção services, definimos 03 contêineres que desejamos lançar. Em primeiro lugar, definimos em servicesveos1.
veos1:
    image: veos:4.18.10M
    container_name: veos1
    privileged: true
    ports:
      - "9001:22"
    network_mode: default
  • veos1
    • Nome do nosso serviço onde definimos contêiner.
  • image: veos:4.18.10Mveos:4.18.10M
    • Queremos que nosso contêiner use imagem.
  • container_name: veos1
    • Substitua o nome do contêiner padrão para facilitar a referência posterior.
  • privileged: true
    • Precisamos do modo privilegiado para KVM.
  • ports:network_mode: default
    • Dizemos ao Docker Compose para usar a porta 9001 do host para se conectar à porta 22 no contêiner.
    • Pedimos ao Compose para usar a ponte Docker padrão, por padrão o Docker Compose criaria uma rede separada e o Vrnetlab não gosta disso. A definição de veos2 é a mesma, exceto que mapeamos a porta 22 para a porta 9002 no host.
  • Finalmente, temos vr-xcon. Itens de nota aqui:
    • links:-linkvr-xcon
      • Equivalente ao argumento que usamos ao executar o contêiner manualmente. Os contêineres listados serão vinculados ao contêiner.
    • command: --p2p veos1/2--veos2/2depends_on:veos1veos2
      • É assim que o comando é passado para o contêiner ao usar o Docker Compose.
      • Dizemos ao Compose para aguardar o serviço listado, aqui veos1 e veos2 antes de iniciar o serviço vr-xcon.

Com tudo isso instalado, estamos prontos para lançar nosso laboratório virtual usando o comando docker-compose up -d, executado no diretório que contém docker-compose.yml. Opção -d faz com que o Compose seja executado em segundo plano.

Para checarmos parâmetros do router que está sendo executado pelo contêiner Docker, iremos executar o seguinte comando docker inspect <name_conteiner>:

 :~/netdev/vrnetlab$ docker inspect veos1
[
    {
        "Id": "812a511d67acef259d32c0ff5c663875dce01e2d2f5333c2ab8a02f94e7fd1a0",
        "Created": "2020-11-23T18:30:00.839862792Z",
        "Path": "/launch.py",
        "Args": [],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 11953,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2020-11-23T18:30:01.536055525Z",
            "FinishedAt": "0001-01-01T00:00:00Z",
            "Health": {
                "Status": "healthy",
                "FailingStreak": 0,
                "Log": [
                    {
                        "Start": "2020-11-23T23:58:46.083872099-03:00",
                        "End": "2020-11-23T23:58:46.30971068-03:00",
                        "ExitCode": 0,
                        "Output": "running\n"
                    },
                    {
                        "Start": "2020-11-23T23:59:16.326774884-03:00",
                        "End": "2020-11-23T23:59:16.530025261-03:00",
                        "ExitCode": 0,
                        "Output": "running\n"
                    },
                    {
                        "Start": "2020-11-23T23:59:46.547713439-03:00",
                        "End": "2020-11-23T23:59:46.717139617-03:00",
                        "ExitCode": 0,
                        "Output": "running\n"
                    },
                    {
                        "Start": "2020-11-24T00:00:16.750926346-03:00",
                        "End": "2020-11-24T00:00:16.890863143-03:00",
                        "ExitCode": 0,
                        "Output": "running\n"
                    },
                    {
                        "Start": "2020-11-24T00:00:46.913430867-03:00",
                        "End": "2020-11-24T00:00:47.064266142-03:00",
                        "ExitCode": 0,
                        "Output": "running\n"
                    }
                ]
            }
        },
        "Image": "sha256:12915790e700351103bf29a327ed7c1834bdee80d1b51f701ef1f2da483e7e7d",
        "ResolvConfPath": "/var/lib/docker/containers/812a511d67acef259d32c0ff5c663875dce01e2d2f5333c2ab8a02f94e7fd1a0/resolv.conf",
        "HostnamePath": "/var/lib/docker/containers/812a511d67acef259d32c0ff5c663875dce01e2d2f5333c2ab8a02f94e7fd1a0/hostname",
        "HostsPath": "/var/lib/docker/containers/812a511d67acef259d32c0ff5c663875dce01e2d2f5333c2ab8a02f94e7fd1a0/hosts",
        "LogPath": "/var/lib/docker/containers/812a511d67acef259d32c0ff5c663875dce01e2d2f5333c2ab8a02f94e7fd1a0/812a511d67acef259d32c0ff5c663875dce01e2d2f5333c2ab8a02f94e7fd1a0-json.log",
        "Name": "/veos1",
        "RestartCount": 0,
        "Driver": "overlay2",
        "Platform": "linux",
        "MountLabel": "",
        "ProcessLabel": "",
        "AppArmorProfile": "unconfined",
        "ExecIDs": null,
        "HostConfig": {
            "Binds": null,
            "ContainerIDFile": "",
            "LogConfig": {
                "Type": "json-file",
                "Config": {}
            },
            "NetworkMode": "default",
            "PortBindings": {},
            "RestartPolicy": {
                "Name": "no",
                "MaximumRetryCount": 0
            },
            "AutoRemove": false,
            "VolumeDriver": "",
            "VolumesFrom": null,
            "CapAdd": null,
            "CapDrop": null,
            "Capabilities": null,
            "Dns": [],
            "DnsOptions": [],
            "DnsSearch": [],
            "ExtraHosts": null,
            "GroupAdd": null,
            "IpcMode": "private",
            "Cgroup": "",
            "Links": null,
            "OomScoreAdj": 0,
            "PidMode": "",
            "Privileged": true,
            "PublishAllPorts": false,
            "ReadonlyRootfs": false,
            "SecurityOpt": [
                "label=disable"
            ],
            "UTSMode": "",
            "UsernsMode": "",
            "ShmSize": 67108864,
            "Runtime": "runc",
            "ConsoleSize": [
                0,
                0
            ],
            "Isolation": "",
            "CpuShares": 0,
            "Memory": 0,
            "NanoCpus": 0,
            "CgroupParent": "",
            "BlkioWeight": 0,
            "BlkioWeightDevice": [],
            "BlkioDeviceReadBps": null,
            "BlkioDeviceWriteBps": null,
            "BlkioDeviceReadIOps": null,
            "BlkioDeviceWriteIOps": null,
            "CpuPeriod": 0,
            "CpuQuota": 0,
            "CpuRealtimePeriod": 0,
            "CpuRealtimeRuntime": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "Devices": [],
            "DeviceCgroupRules": null,
            "DeviceRequests": null,
            "KernelMemory": 0,
            "KernelMemoryTCP": 0,
            "MemoryReservation": 0,
            "MemorySwap": 0,
            "MemorySwappiness": null,
            "OomKillDisable": false,
            "PidsLimit": null,
            "Ulimits": null,
            "CpuCount": 0,
            "CpuPercent": 0,
            "IOMaximumIOps": 0,
            "IOMaximumBandwidth": 0,
            "MaskedPaths": null,
            "ReadonlyPaths": null
        },
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/e3e7cde68f2f3d9b77b5d0cc9bed1c5f280933e6026d4cd9782dbf107dfc010e-init/diff:/var/lib/docker/overlay2/58675c879eaf211b096100236b95eb628e42eaa0188487fdf49ed725da53fc71/diff:/var/lib/docker/overlay2/aef9c9cb1735e9d5506a97136ad8f48957ee74e5567950d39c339e7f4cc9ab57/diff:/var/lib/docker/overlay2/61908948e0b5f02c1c5f8cbf6585595698b7854cd1cbb957562b786f998431c0/diff:/var/lib/docker/overlay2/4964b80c7ffcab358a52aa13485530e5801527eaaecb88701007ac31f7205c38/diff:/var/lib/docker/overlay2/dd0db7422a822df51147fc3aa95916b4f750865111a6057a45cb5c7a56a2950c/diff",
                "MergedDir": "/var/lib/docker/overlay2/e3e7cde68f2f3d9b77b5d0cc9bed1c5f280933e6026d4cd9782dbf107dfc010e/merged",
                "UpperDir": "/var/lib/docker/overlay2/e3e7cde68f2f3d9b77b5d0cc9bed1c5f280933e6026d4cd9782dbf107dfc010e/diff",
                "WorkDir": "/var/lib/docker/overlay2/e3e7cde68f2f3d9b77b5d0cc9bed1c5f280933e6026d4cd9782dbf107dfc010e/work"
            },
            "Name": "overlay2"
        },
        "Mounts": [],
        "Config": {
            "Hostname": "812a511d67ac",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "10000/tcp": {},
                "10001/tcp": {},
                "10002/tcp": {},
                "10003/tcp": {},
                "10004/tcp": {},
                "10005/tcp": {},
                "10006/tcp": {},
                "10007/tcp": {},
                "10008/tcp": {},
                "10009/tcp": {},
                "10010/tcp": {},
                "10011/tcp": {},
                "10012/tcp": {},
                "10013/tcp": {},
                "10014/tcp": {},
                "10015/tcp": {},
                "10016/tcp": {},
                "10017/tcp": {},
                "10018/tcp": {},
                "10019/tcp": {},
                "10020/tcp": {},
                "10021/tcp": {},
                "10022/tcp": {},
                "10023/tcp": {},
                "10024/tcp": {},
                "10025/tcp": {},
                "10026/tcp": {},
                "10027/tcp": {},
                "10028/tcp": {},
                "10029/tcp": {},
                "10030/tcp": {},
                "10031/tcp": {},
                "10032/tcp": {},
                "10033/tcp": {},
                "10034/tcp": {},
                "10035/tcp": {},
                "10036/tcp": {},
                "10037/tcp": {},
                "10038/tcp": {},
                "10039/tcp": {},
                "10040/tcp": {},
                "10041/tcp": {},
                "10042/tcp": {},
                "10043/tcp": {},
                "10044/tcp": {},
                "10045/tcp": {},
                "10046/tcp": {},
                "10047/tcp": {},
                "10048/tcp": {},
                "10049/tcp": {},
                "10050/tcp": {},
                "10051/tcp": {},
                "10052/tcp": {},
                "10053/tcp": {},
                "10054/tcp": {},
                "10055/tcp": {},
                "10056/tcp": {},
                "10057/tcp": {},
                "10058/tcp": {},
                "10059/tcp": {},
                "10060/tcp": {},
                "10061/tcp": {},
                "10062/tcp": {},
                "10063/tcp": {},
                "10064/tcp": {},
                "10065/tcp": {},
                "10066/tcp": {},
                "10067/tcp": {},
                "10068/tcp": {},
                "10069/tcp": {},
                "10070/tcp": {},
                "10071/tcp": {},
                "10072/tcp": {},
                "10073/tcp": {},
                "10074/tcp": {},
                "10075/tcp": {},
                "10076/tcp": {},
                "10077/tcp": {},
                "10078/tcp": {},
                "10079/tcp": {},
                "10080/tcp": {},
                "10081/tcp": {},
                "10082/tcp": {},
                "10083/tcp": {},
                "10084/tcp": {},
                "10085/tcp": {},
                "10086/tcp": {},
                "10087/tcp": {},
                "10088/tcp": {},
                "10089/tcp": {},
                "10090/tcp": {},
                "10091/tcp": {},
                "10092/tcp": {},
                "10093/tcp": {},
                "10094/tcp": {},
                "10095/tcp": {},
                "10096/tcp": {},
                "10097/tcp": {},
                "10098/tcp": {},
                "10099/tcp": {},
                "161/udp": {},
                "22/tcp": {},
                "443/tcp": {},
                "5000/tcp": {},
                "80/tcp": {},
                "830/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "DEBIAN_FRONTEND=noninteractive"
            ],
            "Cmd": null,
            "Healthcheck": {
                "Test": [
                    "CMD",
                    "/healthcheck.py"
                ]
            },
            "Image": "veos:4.18.10M",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": [
                "/launch.py"
            ],
            "OnBuild": null,
            "Labels": {}
        },
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "4cfbd9467fabc4d8c70debce49b5990c0c1d6313e95ba44179d6d082d3974bc9",
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "Ports": {
                "10000/tcp": null,
                "10001/tcp": null,
                "10002/tcp": null,
                "10003/tcp": null,
                "10004/tcp": null,
                "10005/tcp": null,
                "10006/tcp": null,
                "10007/tcp": null,
                "10008/tcp": null,
                "10009/tcp": null,
                "10010/tcp": null,
                "10011/tcp": null,
                "10012/tcp": null,
                "10013/tcp": null,
                "10014/tcp": null,
                "10015/tcp": null,
                "10016/tcp": null,
                "10017/tcp": null,
                "10018/tcp": null,
                "10019/tcp": null,
                "10020/tcp": null,
                "10021/tcp": null,
                "10022/tcp": null,
                "10023/tcp": null,
                "10024/tcp": null,
                "10025/tcp": null,
                "10026/tcp": null,
                "10027/tcp": null,
                "10028/tcp": null,
                "10029/tcp": null,
                "10030/tcp": null,
                "10031/tcp": null,
                "10032/tcp": null,
                "10033/tcp": null,
                "10034/tcp": null,
                "10035/tcp": null,
                "10036/tcp": null,
                "10037/tcp": null,
                "10038/tcp": null,
                "10039/tcp": null,
                "10040/tcp": null,
                "10041/tcp": null,
                "10042/tcp": null,
                "10043/tcp": null,
                "10044/tcp": null,
                "10045/tcp": null,
                "10046/tcp": null,
                "10047/tcp": null,
                "10048/tcp": null,
                "10049/tcp": null,
                "10050/tcp": null,
                "10051/tcp": null,
                "10052/tcp": null,
                "10053/tcp": null,
                "10054/tcp": null,
                "10055/tcp": null,
                "10056/tcp": null,
                "10057/tcp": null,
                "10058/tcp": null,
                "10059/tcp": null,
                "10060/tcp": null,
                "10061/tcp": null,
                "10062/tcp": null,
                "10063/tcp": null,
                "10064/tcp": null,
                "10065/tcp": null,
                "10066/tcp": null,
                "10067/tcp": null,
                "10068/tcp": null,
                "10069/tcp": null,
                "10070/tcp": null,
                "10071/tcp": null,
                "10072/tcp": null,
                "10073/tcp": null,
                "10074/tcp": null,
                "10075/tcp": null,
                "10076/tcp": null,
                "10077/tcp": null,
                "10078/tcp": null,
                "10079/tcp": null,
                "10080/tcp": null,
                "10081/tcp": null,
                "10082/tcp": null,
                "10083/tcp": null,
                "10084/tcp": null,
                "10085/tcp": null,
                "10086/tcp": null,
                "10087/tcp": null,
                "10088/tcp": null,
                "10089/tcp": null,
                "10090/tcp": null,
                "10091/tcp": null,
                "10092/tcp": null,
                "10093/tcp": null,
                "10094/tcp": null,
                "10095/tcp": null,
                "10096/tcp": null,
                "10097/tcp": null,
                "10098/tcp": null,
                "10099/tcp": null,
                "161/udp": null,
                "22/tcp": null,
                "443/tcp": null,
                "5000/tcp": null,
                "80/tcp": null,
                "830/tcp": null
            },
            "SandboxKey": "/var/run/docker/netns/4cfbd9467fab",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "fc7393c03a71a38774dfa3e7af807f40f702474b4263f391e08879de18bc3037",
            "Gateway": "172.17.0.1",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "MacAddress": "02:42:ac:11:00:02",
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "4d8c0864e23de306156f1541bdfc181ab4541b774e5190fe7dadb589f14607e6",
                    "EndpointID": "fc7393c03a71a38774dfa3e7af807f40f702474b4263f391e08879de18bc3037",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02",
                    "DriverOpts": null
                }
            }
        }
    }
]

Os dados mais importantes a serem analisados são os últimos, onde podemos visualizar por exemplo, a chave (IPAddress) e o valor (172.17.0.2) "IPAddress": "172.17.0.2" onde seria o endereço para acessarmos via SSH.

Pronto, imagens devidamente executadas e rodando via Docker, agora iremos montar nossa estrutura de testes automatizados integrados com o Github Action.

Inicializando o GitHub Action

Para mostrar a vocês como integrar sua plataforma de laboratório local (neste caso, baseado em Vrnetlab), vamos usar este repositório simples que criei no GitHub:

Para termos uma ação no GitHub, precisaríamos ter um diretório .github/workflows/ e dentro deste diretório armazenar um arquivo do tipo fluxo de trabalho. Neste repositório contém o arquivo main.yml:

thiago@thiago:~/hello-github-actions$ ls -l .github/workflows/
total 8
-rw-r--r-- 1 thiago thiago 678 nov 23 18:16 main.yml

E se verificarmos o arquivo main.yml, veremos toda a magia neste arquivo:

thiago@thiago:~/hello-github-actions$ cat .github/workflows/main.yml 
---

name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: self-hosted # INFO AO GIT QUE ESTE FLUXO DE TRABALHO É EXECUTADO LOCAL
    steps:
      - uses: actions/checkout@v2 (TIRAR DÚVIDA SOBRE ESSA LINHA)

      - name: Run playbook
        uses: dawidd6/action-ansible-playbook@v2 (TIRAR DÚVIDA SOBRE ESSA LINHA)
        with:
          playbook: vrnetlab.yml
          directory: ./
          inventory: |
            [all]
            veos1 ansible_host="172.17.0.2" ansible_ssh_user="vrnetlab" ansible_ssh_pass="VR-netlab9" ansible_connection="network_cli" ansible_network_os="eos"
            veos2 ansible_host="172.17.0.3" ansible_ssh_user="vrnetlab" ansible_ssh_pass="VR-netlab9" ansible_connection="network_cli" ansible_network_os="eos"

          options: |
            --verbose

Vamos verificar este arquivo com mais detalhes…

Há um nome de fluxo de trabalho, e também definimos em quais ramificações / ações do git esse fluxo de trabalho será executado:

name: CI
on: [push, pull_request]

Ponto importante: informamos ao GitHub que este fluxo de trabalho deve ser executado em nosso ambiente local. Posteriormente, instalaremos um executor GitHub auto-hospedado em nossa máquina:

runs-on: self-hosted

Para testar um Ansible Playbook simples, usamos a ação predefinida disponível para todos no GitHub Action Marketplace: dawidd6/action-ansible-playbook@v2.

Iremos rodar um playbook simples apenas para teste da integração continua, segue o manual descrito abaixo:

:~/hello-github-actions$ cat vrnetlab.yml 
---

- name: Simple GitHub Action playbook
  hosts: ansible

  vars: # Variável de conexão
    ansible_connection: network_cli
    ansible_network_os: eos
    ansible_user: vrnetlab
    ansible_ssh_pass: VR-netlab9
  tasks:
    - name: playbook task1
      eos_command:
        commands: show ip int brief

Comandos Git básicos

Nesta seção, iremos desmistificar alguns comandos básicos que iremos utilizar para interagir com o nosso repositório local e remoto:

  • git add -A
    • É útil para você mudar algo ou alterar algum arquivo ou alocar algum arquivo ao diretório.
  • git commit -m "teste"
    • valida a alteração dentro do repositório para sincronizar com o repositório na cloud.

Dica: commit confirmará as alterações no repositório local, enquanto o push enviará as alterações para um repositório remoto.

  • git pull origin master
    • Valida a mudança dentro da branch master
  • git push origin master
    • Sincroniza a mudança local para remoto

Dica: git push, git pull - Sincroniza o repositório local com seu repositório remoto associado . push - aplicar alterações de local para remoto, pull - aplicar alterações de remoto para local.

GitHub Runner auto-hospedado

Este recurso faz com que sua máquina em execução se conecta ao GitHub usando o aplicativo de execução auto-hospedado GitHub Actions.

O runner é apenas um programa simples escrito em .NET (mesmo na versão Linux) que é de código aberto. Ele é executado em sua máquina local como outro programa de usuário. Ele terá acesso a todas as bibliotecas, binários e ferramentas instaladas em sua máquina.

Por exemplo, se o seu código precisa executar o Ansible, então sua máquina deve ter instalado o Ansible, para que o executor possa usá-lo.

Para utilizarmos o recurso do runner de forma local, precisamos ir no repositório onde estão nossos arquivos, ir em settings - action - add runner:

alt

Após clicar em add runner, é só seguir as instruções sugeridas pelo GitHub.

Download:

thiago@thiago:~$ mkdir actions-runner && cd actions-runner
thiago@thiago:~/actions-runner$
thiago@thiago:~/actions-runner$ curl -O -L https://github.com/actions/runner/releases/download/v2.274.2/actions-runner-linux-x64-2.274.2.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   665  100   665    0     0   1027      0 --:--:-- --:--:-- --:--:--  1026
100 70.4M  100 70.4M    0     0   408k      0  0:02:56  0:02:56 --:--:--  646k
thiago@thiago:~/actions-runner$
thiago@thiago:~/actions-runner$ tar xzf ./actions-runner-linux-x64-2.274.2.tar.gz

Configure:

thiago@thiago:~/actions-runner$ ./config.sh --url https://github.com/tporfirio/hello-github-actions --token AO3UA2D23JKRGAH2EDTHWMK7XQK2G

--------------------------------------------------------------------------------
|        ____ _ _   _   _       _          _        _   _                      |
|       / ___(_) |_| | | |_   _| |__      / \   ___| |_(_) ___  _ __  ___      |
|      | |  _| | __| |_| | | | | '_ \    / _ \ / __| __| |/ _ \| '_ \/ __|     |
|      | |_| | | |_|  _  | |_| | |_) |  / ___ \ (__| |_| | (_) | | | \__ \     |
|       \____|_|\__|_| |_|\__,_|_.__/  /_/   \_\___|\__|_|\___/|_| |_|___/     |
|                                                                              |
|                       Self-hosted runner registration                        |
|                                                                              |
--------------------------------------------------------------------------------

# Authentication

√ Connected to GitHub

# Runner Registration

Enter the name of runner: [press Enter for thiago] 

This runner will have the following labels: 'self-hosted', 'Linux', 'X64' 
Enter any additional labels (ex. label-1,label-2): [press Enter to skip] 

√ Runner successfully added
√ Runner connection is good

# Runner settings

Enter name of work folder: [press Enter for _work] 

√ Settings Saved.

thiago@thiago:~/actions-runner$ ./run.sh

√ Connected to GitHub

2020-11-23 19:11:50Z: Listening for Jobs

Se tudo tiver ok, você verá seu runner listado como idle:

alt

Execução de ação GitHub

Portanto, agora temos tudo pronto para testar o ambiente de laboratório: Os roteadores, o repositório GitHub e o fluxo de trabalho de ação e o Runner na máquina local. Assim que tivermos um push / commit dentro do repositório do projeto após ser feita alguma alteração no playbook Ansible, poderemos ver como um pipeline CI, neste caso:

alt

Recapitulação e resumo

OK, até agora tudo bem, vamos resumir as etapas que seguimos nesta postagem do blog:

  1. Configuramos um laboratório local simples usando vrnetlab e docker, com dois roteadores funcionando localmente.
  2. Nós criamos um repositório Github simples, com uma ação GitHub simples, baseado em Ansible.
  3. Instalamos um runner do GitHub na máquina local e vinculamos esse runner à ação do GitHub.
  4. Enviamos um commit para o repositório GiiHub e o GitHub lançou o Action, neste caso, uma “versão de show” foi executada com um Ansible Playbook

Portanto, com um pc / laptop de máquina local decente, você pode executar seu próprio ambiente de laboratório de rede e usá-lo de plataformas CI / CD como GitHub ou Gitlab e testar seu código com muita facilidade.

Nos próximos posts, farei uso desse ambiente para falar sobre o Desenvolvimento Orientado a Testes no campo NetDevOps.

Fique ligado.

Leave a Comment