Technique
Configuration automatique d'un switch Allied Telesis en Go

Allied Telesis (AT) est un constructeur historique d'équipement réseau, peu connu sur le marché, en particulier en Europe. Ses filiales principales sont situées au Japon et aux Etats-Unis. Son chiffre d'affaires est probablement à peine 1% de celui des plus grands fabricants d'équipements réseau.

Il propose cependant une gamme d'équipements intéressantes et innovantes, à prix bien positionné. Les switches AT, par exemple la série X230, sont parmi les switches compatibles SDN et open flow les moins chers. Tous les switches managés AT sont en fait compatibles SDN, comme précisé sur le site Allied Telesis. L'acquisition d'une license est nécessaire pour activer ces fonctionnalités. Ces possibilités sont très intéressantes dans divers situations. Vous pouvez par exemple tester et déployer de nouvelles fonctionnalités, comme du filtrage de trafic ou l'implémentation de nouveaux mécanismes au niveau du LAN.

Interfaces de programmation des switches AT

Nous avons rencontré ces switches pour la première fois dans une infrastructure mise en place pour tester des mécanismes SDN. Comme nous nous efforçons de manager systématiquement les équipements en mode devops, nous avons cherché s'il existait une API pour gérer ces switches. Malheureusement, aucun module ansible n'existe à notre connaissance pour ces switches et il n'offrent pas non plus d'API REST. D'autres switches AT fournissent une API REST, voir ici par exemple.

La seule solution pour éviter une configuration manuelle consiste à écrire un programme pour aller automatiquement modifier la configuration via une session SSH. Dans la suite de ce billet, nous vous présentons nos premiers retours d'expérience sur cette méthode.

Go, une alternative à python pour les programmes Devops ?

Dans le domaine Devops, Python est un standard de fait. Nous avons choisi, pour cette expérimentation, de travailler avec Go, parce que nous avons eu des retours comme quoi des équipes devops renommées avaient choisi Go pour de bonnes raisons :

  • La compilation est très rapide et quasi-transparente par rapport à un langage scripté
  • Le code binaire prend moins de place et s'exécute plus vite
  • Go est un langage mieux structuré qui offre de meilleurs garanties contre les bugs, vérification de types, allocation de mémoire, gestion des variables

Comparé à Python, la productivité du développement en Go est certainement moindre, parce que beaucoup de librairies qui permettent d'écrire les choses rapidement sont absentes. Cependant, nous pensons que les langages compilés devraient progressivement reprendre du terrain, parce que la compilation n'est plus réellement un souci et que ces langages gardent une avance en terme de performance.

Vous trouverez dans la suite de ce billet les codes sources utilisés pour ces premiers tests.

Installer Go avec Ansible

Comme la plupart des programmes sous Linux, il est possible d'installer Go au niveau système ou dans l'espace utilisateur. Dans ce tutoriel, nous l'installons dans l'espace d'un utilisateur.

La procédure d'installation manuelle est publiée sur le site officiel de Go : https://golang.org/doc/install.

Comme nous gérons l'ensemble de nos systèmes avec Ansible, nous avons traduit cette procédure sous forme d'un rôle Ansible :

 

# roles/golang/tasks
# golang
- name: check if golang has already been downloaded
  stat:
    path: "{{working_directory}}/{{golang_archive_file}}"
  register: golang
- name: download golang installation archive
  get_url:
    url: "https://dl.google.com/go/{{golang_archive_file}}"
    dest: "{{working_directory}}"
    sha256sum: "{{golang_sha256}}"
  when: golang.stat.exists == False
- name: test if golang is already installed
  stat:
    path: "/home/{{homeuser}}/go"
  register: golang_installed
- name: install go in home directory
  unarchive:
    src: "{{working_directory}}/{{golang_archive_file}}"
    dest: "/home/{{homeuser}}"
    owner: "{{homeuser}}"
    group: "{{homeuser}}"
    mode: '755'
  when: golang_installed.stat.exists == False
- name: add golang to bashrc for {{homeuser}}
  lineinfile:
    path: "/home/{{homeuser}}/.bashrc"
    regexp: 'go/bin'
    line: "export PATH=\"/home/{{homeuser}}/go/bin:$PATH\""
    state: present
    insertafter: EOF
- name: set GOPATH environment variable in bashrc
  lineinfile:
    path: "/home/{{homeuser}}/.bashrc"
    regexp: 'GOPATH'
    line: "export GOPATH=\"/home/{{homeuser}}/go/bin\""
    state: present
    insertafter: EOF

Le rôle d'installation dépend des variables suivantes :

Variable Role
working_directory Le répertoire de travail utilisé par Ansible pour des packets à installer
homeuser Le nom d'utilisateur (et le répertoire home) de l'utilisateur pour qui Go est installé
golang_archive_file Le nom du fichier archive téléchargé sur le site de Go
golang_sha256 Le sha256 du fichier d'archive

Nous avons initialisé les variables dans un fichier par défaut associé au rôle Go défini dans Ansible :

# roles/golang/defaults/main.yml
golang_archive_file: go1.13.5.linux-amd64.tar.gz
golang_sha256: 512103d7ad296467814a6e3f635631bd35574cab3369a97a323c9a585ccaa569

Il vous suffit ensuite de créer les répertoires ad hoc, de copier-coller les fichiers ci-dessus et d'ajouter le rôle correspondant à votre playbook pour installer Go.

Vous trouverez également ces fichiers dans notre répertoire Github. Ce n'est pas un développement très idiomatiquement ansible mais il fait le travail.

Ajouter le support de Go à Visual Studio Code

Nous travaillons avec l'IDE Microsoft Visual Studio Code, un outil Microsoft gratuit, open source et très efficaces pour développer. Visual Studio Code détecte automatiquement les fichiers go ayant l'extension .go et adapte sa configuration pour faciliter la tâche du développeur dès que le GOPATH est valorisé, comme nous l'avons fait dans notre fichier ansible. Dans notre environnement, la variable GOPATH est initialisée dans le fichier .bashrc dans le home directory de l'utilisateur.

Se connecter à un switch AT via SSH en Go

Go comprend une libraire SSH. La documentation est disponible ici. De nombreux tutoriels présentes comment se servir de cette librairie, dont peu fonctionnent réellement. Nous en avons testé plusieurs avant de trouver celui-ci qui nous a semblé le plus simple et le meilleur.

Gérer les clés d'authenfication sur le client

Les switches ont été configurés pour accepter l'authentification SSH par clé. On pourrait penser qu'il est plus simple d'utiliser un login/mot de passe. En pratique, gérer l'authentification de programmes par login/mot de passe avec un niveau de sécurité acceptable est une opération complexe :

  • Stockage du login/mot de passe dans fichier protégé et chiffré
  • Valorisation d'une variable d'environnement appelée par le programme

Ecrire les login/mot de passe en dur dans les fichiers de configuration n'est pas trop envisageable! Au bout du compte, l'authentification par clé publique est plus facile à gérer. Vous pouvez commencer avec des clés auto-signées et évoluer, à mesure que votre aisance augmente, construire une infrastructure de gestion de clés légère, avec des scripts pour générer les différents types de certificats.

Un certain nombre de particularités de la librairie SSH de Go méritent d'être soulignées :

  • Si vous protégez les clés privées par mot de passe, elles sont stockées chiffrées. Vous devez donc soit produire le mot de passe à l'exécution, ou charger les clés en mémoire au moment du boot ou dans un script post-boot. Dans les distributions linux, le chargement des clés est géré par ssh-agent
  • Si vous travaillez avec Ubuntu, ssh-agent est remplacer par keyrings. Vous n'avez pas besoin d'installer ssh-agent. Keyrings est installé par défaut et publie la même API que ssh-agent.
  • Les dernières versions de Go valide le certificat du serveur. Voir ici et ici pour plus d'information. Pour tester la connexion SSH? vous pouvez bypasser temporairement la vérification du certificat. En production, ce n'est normalement pas recommandé.

Au démarrage du programme, vous vérifiez que la clé privée utilisée pour s'authentifier sur le switch est bien chargée :

// SSHAgent returns ssh keys registered in the user memory key ring
func SSHAgent() ssh.AuthMethod {
	if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
		return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
	}
	return nil
}

Génération une clé serveur autosignée sur le switch

atswitch# crypto key generate rsa 4096

Activation de l'authentification par clé sur le switch

atswitch> enable
atswitch# configure terminal
atswitch# ssh server v2only
atswitch# ssh server authentication publickey
atswitch# no ssh server authentication password
atswitch# ssh server allow-users <USERNAME>
atswitch# service ssh
atswitch# ssh server scp

Copie de la clé publique du client sur le switch par TFTP

Vous devez d'abord générer un couple clé privée-clé publique pour le client. La clé publique est ensuite stockée dans un répertoire protégé (avec les bonnes permissions pour qu'elle soit accepté par ssh-agent ou keyrings). La clé publique est déposée sur un serveur TFTP. Elle est ensuite uploadée sur le switch via TFTP et rattachée à l'utilisateur ad-hoc :

atswitch# copy tftp://<PATH TO THE KEY> flash:/lankey.pub
atswitch# conf t
atswitch# crypto key pubkey-chain userkey <USERNAME> lankey.pub
atswitch# exit

Debug de la connexion SSH

Dans notre exemple, le test de connexion renvoyait le message d'erreur suivant sur le client : Too many authentication failures for USERNAME.

Pour trouver l'origine du problème, nous avons activé le mode debug sur le switch :

atswitch# en
atswitch# conf t
atswitch# debug ssh server brief
atswitch# exit
atswitch# terminal monitor

Nous avons ensuite trouvé l'origine du problème dans les logs :

16:26:01 awplus sshd[11841]: Authentication refused: bad ownership or modes for directory /flash/.home/<USERNAME>/.ssh

Notre méthode d'upload de la clé publique a généré un problème étrange au niveau du système de fichier (linux ou bsd) sous-jacent du switch. Nous avons réglé ce problème en désenregistrant puis réenregistrant la clé comme suit :

atswitch# en
atswitch# cd flash:
atswitch# cd .home/
atswitch# rmdir force <USERNAME>
atswitch# cd ..
atswitch# conf t
atswitch# crypto key pubkey-chain userkey <USERNAME> lankey.pub
atswitch# exit

La connexion SSH fonctionnait ensuite.

Connection au switch avec le programme Go

Se connecter au switch est identique à se connecter à un autre serveur SSH. Le code des différentes étapes de connexion peut être trouvé dans ce tutoriel. Les principales étapes sont

  1. Instancier et configurer le client
  2. Se connecter au serveur
  3. Créer une session
  4. Rediriger les entrées/sorties standard pour permettre l'envoi de commande et le retour des réponses
  5. Ouvrir un shell distant

Pour tester notre approche, nous avons modifié le code pour envoyer des commandes au switch :

	commands := []string{
		"enable",
		"configure terminal",
		"interface port1.0.1-1.0.10",
		"description lan port",
		"exit",
		"no ip route 0.0.0.0/0 192.168.123.1",
		"ip route 0.0.0.0/0 192.168.123.254",
		"exit",
		"write mem",
		"exit",
	}
	for _, cmd := range commands2 {
		_, err = fmt.Fprintf(stdin, "%s\n", cmd)
		if err != nil {
			log.Fatal(err)
		}
  }

Réflexions sur ce test

Gérer des équipements semble pouvoir tenir dans quelques lignes de Go. Il y a encore beaucoup de travail à faire pour passer de ce premier test à un programme prêt pour la production:

  1. Gestion des connexions SSH
    Lors de la configuration d'équipements, et particulièrement des équipements réseau, il est parfois nécessaire de réinitialiser les interfaces réseau. La connexion SSH tombe puis se reconnecte, ou pas, en fonction de la manière dont elle est gérée. Un programme en production doit savoir réagir à ces événements pour permettre une reconnexion après déconnexion.
    Plus généralement, les timeouts TCP sont longs. Le client risque de détecter la perte de connexion après plusieurs secondes voire plusieurs minutes. Il sera donc nécessaire de tuner ces paramètres pour améliorer la détection et la réaction aux fautes réseau. network fault detection.

  2. Commit des changements de configuration
    Dans l'exemple, nous ne vérifions pas que les changements sont bien effectués et persistants :
    • Envoi des commandes
    • Vérification des valeurs de retour
    • Commit de la version obtenu après n commandes et sauvegarde en mémoire.
      Il y a donc un travail important de validation du dialogue et temporisation des commandes pour passer d'un état initial stable à un état final stable.

  3. Idempotence
    Si l'on exécute le programme une deuxième fois, rien n'est prévu pour s'assurer que la configuration ne changera pas. En production, une forme d'idempotence est nécessaire.