OpenStack propose une multitude d’API permettant de manipuler les différents services installés. D’ailleurs, les différents modules d’OpenStack communiquent pour la plupart via les API. L’idée dans cet article est d’utiliser les API Python pour créer un réseau virtuel comportant divers éléments.
Nous allons voir comment créer un réseau virtuel en développant un script utilisant les API Python d’OpenStack. Notre infrastructure virtuelle sera composée de deux réseaux constitués pour chacun d’un serveur. Ces deux réseaux seront connectés au même routeur et devront pouvoir communiquer ensemble. On se limitera aux requêtes ICMP. Ci-dessous, voici le schéma de l’objectif.
Pour manipuler les API, la documentation d’OpenStack est fortement utile. Vous pouvez aussi vous aider de code existant. Par exemple, Horizon utilise la plupart des API des principaux modules. https://github.com/openstack/horizon/tree/stable/newton/openstack_dashboard/api
Avant de commencer à créer des réseaux, instances ou autres, il faut pouvoir se connecter à notre OpenStack. Pour s’authentifier, il faut passer par le module Keystone. Dans l’exemple, la version 3 de Keystone est utilisée et c’est désormais la recommandation puisque la deuxième version est devenue obsolète. On fournit les informations d’authentification indispensable sans oublier de fournir l’URL de l’API Keystone. À noter que la V3 de Keystone demande obligatoirement le project_domain_id et le user_domain_id.
Pour éviter de se connecter à chaque requête, on crée une session qu’on utilisera à chaque fois.
from keystoneauth1 import identity from keystoneauth1 import session auth = identity.Password(username='demo', password='liced', project_name='demo', project_domain_id='default', user_domain_id='default', auth_url='http://192.168.100.2:5000/v3') ma_session = session.Session(auth=auth)
Avant de manipuler les éléments relatifs au réseau, il faut bien entendu importer le client neutron.
from neutronclient.v2_0 import client as neutron_client
Tout d’abord, on commence par créer les deux réseaux qui comporteront les instances en spécifiant dans un dictionnaire leur nom et en les activant. Voici la documentation relative afin de connaître les attributs nécessaires à spécifier pour créer un réseau.
On récupère la réponse pour extraire l’identifiant de chaque réseau créé parce qu’ils seront utiles pour la suite.
network1 = { 'network': { 'name': 'reseau1', 'admin_state_up': True } } net1 = neutron.create_network(body=network1) net1_id = net1['network']['id'] network2 = { 'network': { 'name': 'reseau2', 'admin_state_up': True } } net2 = neutron.create_network(body=network2) net2_id = net2['network']['id']
On peut désormais s’attaquer à la création des sous-réseaux. Il faut dans les données, un identifiant d’un réseau déjà créé (c’est pour cela qu’on a récupéré l’id précédemment), la version du protocole IP et l’adresse du sous-réseau. Nous spécifions également, l’adresse IP de la passerelle même ce n’est pas indispensable (par défaut c’est la première IP disponible du sous-réseau). On récupère aussi les identifiants des sous-réseaux créés pour la suite.
subnet_network1 = { 'subnet': { 'network_id': net1_id, 'ip_version': 4, 'cidr': "192.168.1.1/24", 'gateway_ip': '192.168.1.254', } } sub_net1 = neutron.create_subnet(body=subnet_network1) sub_net1_id = sub_net1['subnet']['id'] subnet_network2 = { 'subnet': { 'network_id': net2_id, 'ip_version': 4, 'cidr': "192.168.2.1/24", 'gateway_ip': '192.168.2.254', } } sub_net2 = neutron.create_subnet(body=subnet_network2) sub_net2_id = sub_net2['subnet']['id']
La création d’un routeur est assez simple.
routeur = { 'router': { 'name': 'routeur', 'admin_state_up': True } } r = neutron.create_router(body=routeur) routeur_id = r['router']['id']
Il nous faut ensuite deux interfaces sur le routeur en créant deux ports. Chaque port doit être lié au routeur, à un réseau et à un sous-réseau. On n’oublie pas d’indiquer l’adresse IP correspondant à la passerelle du réseau en question.
port1 = { 'port': { 'name': 'port1', 'network_id': net1_id, 'fixed_ips': [{'subnet_id': sub_net1_id, 'ip_address': '192.168.1.254'}] } } port2 = { 'port': { 'name': 'port2', 'network_id': net2_id, 'fixed_ips': [{'subnet_id': sub_net2_id, 'ip_address': '192.168.2.254'}] } } port1_id=neutron.create_port(body=port1).get('port')['id'] port2_id=neutron.create_port(body=port2).get('port')['id']
Maintenant que les ports sont créés, il faut les attacher au routeur.
neutron.add_interface_router(routeur_id, {'port_id': port1_id}) neutron.add_interface_router(routeur_id, {'port_id': port2_id})
Il ne reste plus qu’à créer les deux serveurs. Il faut bien sûr utiliser le client Nova pour manipuler les API.
nova = nova_client.Client(version=2.0, session=ma_session)
Étant donné qu’on va utiliser une image pour lancer chaque instance, il nous faut d’abord récupérer l’identifiant de l’image désiré. Utilisant DevStack, j’indique que je souhaite utiliser une des images Cirros proposées par défaut. On récupère également l’identifiant du flavor souhaité.
image = nova.images.find(name='cirros-0.3.4-x86_64-uec') flavor = nova.flavors.find(name='m1.nano')
Pour la création de nos instances nous n’indiquons pas d’IP puisqu’on laisse le service DHCP gérer les adresses. En revanche, il ne faut pas oublier de lier l’instance avec le réseau adéquat.
nova.servers.create(name='instance1', image=image, nics=[{'net-id': net1_id}], flavor=flavor) nova.servers.create(name='instance2', image=image, nics=[{'net-id': net2_id}], flavor=flavor)
À ce stade, notre infrastructure virtualisée est fonctionnelle. On peut se connecter à nos instances par l’intermédiaire de la console VNC d’Horizon. En revanche, il est impossible de « pinger » l’instance2 à partir de l’instance1 et réciproquement. Les requêtes ICMP ne sont pas autorisées.
Par défaut, les instances utilisent la security group default. Il est nécessaire d’ajouter les règles adéquates pour permettre le ping entre le réseau 1 et 2.
sec_default = neutron.list_security_groups(name='default', fields='id') sec_default_id = sec_default['security_groups'][0]['id'] regle1 = { 'security_group_rule': { 'direction': 'ingress', 'protocol': 'icmp', 'remote_group_id': sec_default_id, 'ethertype': 'ipv4', 'security_group_id': sec_default_id } } neutron.create_security_group_rule(regle1) regle2 = { 'security_group_rule': { 'direction': 'egress', 'protocol': 'icmp', 'remote_ip_prefix': '0.0.0.0/0', 'security_group_id': sec_default_id } } neutron.create_security_group_rule(regle2)
L’objectif est atteint et les deux instances peuvent communiquer mutuellement. Le code n’est pas du tout optimisé et il y a moyen de faire les mêmes opérations bien plus proprement. Le but était juste de montrer l’utilisation des API Python pour manipuler un cloud OpenStack. On peut même faire encore plus simple en utilisant le service HEAT, l’orchestrateur d’OpenStack. Avec un template HEAT, quelques lignes de code suffiraient pour réaliser les mêmes opérations.
Réseau virtuel créé par le script