SDK Python pour Cloudwatt

Site officiel : SDK-Development/PythonOpenStackSDK

Documentation : SDK-Development/PythonOpenStackSDK

Remonter un bug : Launchpad


Ce tutoriel a pour but de vous expliquer comment utiliser nos APIs OpenStack pour automatiser le déploiement de vos instances avec la bibliothèque Python officielle d’OpenStack .

Cloudwatt propose une gamme d’APIs puissantes qui peuvent être utilisées pour piloter nos Services Cloud (Serveurs Cloud, Stockage objet, Stockage bloc, Mise en réseau…). Pour en savoir plus, veuillez consulter notre référentiel des APIs Cloudwatt.

Bien qu’il soit possible d’écrire des applications ou des scripts qui émettent des requêtes auprès des APIs appropriées, une grande quantité de code a été centralisé dans des librairies permettant de rendre au développeur cette tâche plus aisée. C’est particulièrement vrai, pour les développeurs Python : OpenStack étant lui-même développé en Python, la communauté a développé des clients Python (“Python client libraries”) qui vous permettent d’accéder aux services OpenStack (Swift, Nova, Neutron…) en lignes de commande.

Installation des clients de librairies Python

Il y a plusieurs manières d’installer les clients. Certaines distributions Linux (comme Ubuntu Trusty) fournissent des paquets permettant d’installer ces clients. Pour les autres distributions, les clients peuvent être installés depuis le dépôt central appelé Python Package Index en utilisant le module pip. Nous allons décrire cette deuxième méthode.

Pour éviter d’installer tous les modules Python via le composant pip, nous allons installer nos paquets dans un environnement virtuel. Nous devons installer les paquets python-virtualenv ainsi que python-pip.

Sur Debian/Ubuntu:

$ sudo apt-get install python-virtualenv python-pip

De plus, l’installation de certains clients requiert certains paquets de développement :

$ sudo apt-get install python-dev libffi-dev libssl-dev

La prochaine étape est de créer un environnement virtuel que nous utiliserons pour installer nos librairies :

$ virtualenv openstack-env
$ source openstack-env/bin/activate

Puis, nous pouvons installer les différents clients :

$ pip install python-cinderclient
$ pip install python-glanceclient
$ pip install python-keystoneclient
$ pip install python-neutronclient
$ pip install python-novaclient
$ pip install python-swiftclient

Afin de vérifier que tous les paquets sont installés avec succès, vous pouvez utiliser les outils en lignes de commande qui ont été installés avec les librairies en déclarant les variables d’environnement pour les Clients en ligne de commande et jouer avec les CLI.

Utilisation des clients de librairies Python

Voici un scénario dont les différentes étapes serviront d’exemple à l’utilisation des librairies Python :

  • Déclarer les credentials
  • Créer un réseau privé
  • Importer une paire de clés SSH
  • Démarrer une instance
  • Allouer une IP flottante
  • Associer l’IP flottante à une instance
  • Créer un volume
  • Attacher le volume à l’instance
  • Redémarrer l’instance
  • Terminer l’instance

Notez que vous pouvez trouver une bonne (bien qu’incomplète) description de ces librairies dans la documentation officielle.

Imports

Voici les classes que nous allons importer dans ce scénario de test :

from cinderclient import client as c_client
from neutronclient.v2_0 import client as q_client
from novaclient.client import Client as n_client

Déclarer les informations d’identification pour utiliser les APIs Cloudwatt

La première chose à faire afin de commencer à travailler avec les API est de retrouver les informations d’identification dans la console Cloudwatt. La méthode la plus simple est de télécharger le fichier RC d’OpenStack mais vous pouvez également le générer manuellement et le configurer.

Les variables USERNAME/PASSWORD que vous allez renseigner vous ont été transmises lors de l’activation de votre compte Cloudwatt. La variable AUTH_URL est fournie dans la console Cloudwatt. Il s’agit de :

  • https://identity.fr1.cloudwatt.com/v2.0

Enfin, notez que vous aurez également besoin d’obtenir le nom du tenant dans lequel vous allez opérer. Il est également disponible via la console.

La première fonction Python que nous allons écrire est une fonction qui formate les informations d’authentification en un dictionnaire qui sera réutilisé plus tard par les différents clients.

Comme vous le remarquerez, les différents clients requièrent différents arguments pour être utilisés. Afin de résoudre ce problème, get_credentials requiert un argument qui est le nom du service qui a besoin des credentials. Ce qui suit n’est aucunement exhaustif mais cela devrait vous donner une idée sur la manière de procéder. Selon votre cas d’usage, vous devrez probablement vous adapter :

def get_credentials(service):
    """Returns a creds dictionary filled with the following keys:

    * username
    * password/api_key (depending on the service)
    * tenant_name/project_id (depending on the service)
    * auth_url

    :param service: a string indicating the name of the service
                    requesting the credentials.
    """

    creds = {}
    # Unfortunately, each of the openstack client will request slightly
    # different entries in their credentials dict.
    if service.lower() in ("nova", "cinder"):
        password = "api_key"
        tenant = "project_id"
    else:
        password = "password"
        tenant = "tenant_name"

    # The most common way to pass these info to your script is to do it through
    # your environment variables. Since we're `get`ing values from a
    # dict, it shouldn't be too difficult to set default values too.
    creds.update({
        "username": os.environ.get('OS_USERNAME', "demo"),
        password: os.environ.get("OS_PASSWORD", "password"),
        "auth_url": os.environ.get("OS_AUTH_URL",
                                    "http://192.168.0.10:5000/v2.0"),
        tenant: os.environ.get("OS_TENANT_NAME", "invisible_to_admin"),
        "region_name": os.environ.get("OS_REGION_NAME", "regionOne"),
    })

    return creds

Dans cet exemple, je fourni à la fonction les informations dont elle a besoin en déclarant des variables d’environnement. Comme vous pouvez vous y attendre, c’est à vous de l’adapter à vos besoin. Les arguments de sécurités ont été ignorées afin de ne pas altérer la lisibilité.

Créer un réseau privé

Cette fonction permet de créer un nouveau réseau privé et une liste de sous-réseaux attachés, en utilisant le client Neutron. Vous remarquerez la manière d’appeler la fonction get_credentials pour l’authentification

    def create_private_network(network_name, cidr_list):
        """Create a private network and subnets. To remain readable this function
        won't do any error checking.

        :param network_name: the name of the private network
        :param cidr_list: an iterable of subnet cidr notations
        """

        credentials = get_credentials("neutron")
        neutron = q_client.Client(**credentials)

        body_sample = {'network': {'name': network_name,
                                   'admin_state_up': True}}

        netw = neutron.create_network(body=body_sample)
        net_dict = netw['network']
        network_id = net_dict['id']

        # We may want to create several subnets in this network
        for cidr in cidr_list:
            body_create_subnet = {'subnets': [{'cidr': cidr,
                                               'ip_version': 4,
                                               'network_id': network_id}]}

            subnet = neutron.create_subnet(body=body_create_subnet)

	    return netw

Connecter un réseau privé à Internet

Vous pouvez connecter un réseau privé à Internet en y attachant un router.

    def create_router(name, private_net_name, public_net_name):
        """Creates a NAT router that will allow VMs on the private net to
        connect to the internet through the public net.

        :param name: Name the router.
        :param private_net_name: Name of the private network where our VMs
                                 will be spawned.
        :param public_net_name: Name of the public network connected to
                                the internet.
        """
        credentials = get_credentials("neutron")
        neutron = q_client.Client(**credentials)

        prv_subnet_id = neutron.list_networks(
            name=private_net_name)['networks'][0]['subnets'][0]
        pub_net_id = neutron.list_networks(
            name=public_net_name)['networks'][0]['id']

        req = {
            "router": {
                "name": name,
                "external_gateway_info": {
                    "network_id": pub_net_id
                },
            "admin_state_up": True
            }
        }
        router = neutron.create_router(body=req)
        router_id = router['router']['id']

        req = {
            "subnet_id": prv_subnet_id
        }
        neutron.add_interface_router(router=router_id, body=req)

        return router

Importer une paire de clés SSH :

La commande suivante vous permet d’importer une paire de clé SSH qui sera utilisée à la création de l’instance pour permettre un accès sans mot de passe à celle-ci.

    def create_keypair(name, key_path):
        """This function imports a public key into nova.

        :param name: the name the keypair is imported to
        :param key_path: the path of the key file on the disk
        """
        creds = get_credentials("nova")
        nova = n_client(version="2", **creds)

        with open(key_path) as key:
			return nova.keypairs.create(name, key.read())

Créer un groupe de sécurité et définir ses règles

Un groupe de sécurité regroupe l’ensemble des règles de filtrage IP (pare-feux) que vous souhaitez appliquer à vos instances. Les groupes de sécurité servent à protéger et limiter l’accès à vos instances. Si aucun groupe de sécurité n’est attaché à une instance, le groupe de sécurité par défaut “default” sera attribué automatiquement à celle-ci. Les règles définies dans ce groupe bloquent tout le trafic entrant et autorisent tout le trafic sortant

Nous allons ici créer un nouveau groupe de sécurité appelé “ssh” avec une règle autorisant les connections SSH via le port 22.

    def create_security_group(sg_name, sg_description):
        """Create a security group

        :param sg_name: Security group name.
        :param sg_description: Security group description.
        """
        creds = get_credentials("nova")
        nova = n_client(version="2", **creds)

        sg_ssh = nova.security_groups.create(sg_name, sg_description)
        nova.security_group_rules.create(sg_ssh.id, ip_protocol="tcp",
                                         from_port="22", to_port="22",
                                         cidr="0.0.0.0/0")

        return sg_ssh

Démarrer une instance

Cette méthode vous permet de retrouver un objet lorsque vous ne connaissez que son nom, en utilisant find()

    def launch_instance(instance_name, flavor_name, image_name, keypair_name,
                        network_label, security_group):

        creds = get_credentials("nova")
        nova = n_client(version="2", **creds)

        # we don't have the object, we only know its name. So we
        # retrieve it like this:
        image = nova.images.find(name=image_name)
        flavor = nova.flavors.find(name=flavor_name)
        network = nova.networks.find(label=network_label)

        return nova.servers.create(name=instance_name, image=image,
                                   flavor=flavor, key_name=keypair_name,
                                   nics=[{'net-id': network.id}],
                                   security_groups=[security_group])

Allouer une IP flottante:

Nous savons comment créer des ressources. Comme vous pouvez vous y attendre, la création une IP flottante est similaire à la création de toute autre ressources (comme les réseaux, les sous-réseaux ou les paires de clés).

Nous allons compléter cette fonction en vérifiant si il y a une IP flottante disponible. Si aucune IP n’est libre, cela engendrera la création d’une nouvelle IP et un retour à l’étape de vérification.

    # First let's check if there are floating ips available. If there are, we'll
    # return a random one. If not, we'll create one and return it.
    def get_or_create_floating_ip(pool=None):

        creds = get_credentials("nova")
        nova = n_client(version="2", **creds)

        ip_list = nova.floating_ips.list()
        # Filtering out associated floating IPs
        ip_list = [ip for ip in ip_list if ip.instance_id is None]
        # Filtering floating IPs accord to a specific pool
        if pool is not None:
            ip_list = [ip for ip in ip_list if ip.pool == pool]

        if len(ip_list) > 0:
			# don't forget to import random
            return random.choice(ip_list)
        else:
            return nova.floating_ips.create(pool)

Associer une IP flottante à une instance:

Cette commande permet d’attacher une IP précise à une instance.

    def attach_floating_ip(server, ip):
        return server.add_floating_ip(ip)

Créer un volume

La création de volumes s’opère de la même manière que les autres resources. Le client Cinder s’initialise un peu différemment de Nova et Neutron.

    def create_volume(size, name=None):

        creds = get_credentials("cinder")
        cinder = c_client.Client(**creds)

        return cinder.volumes.create(size=size, display_name=name)

Attacher le volume à une instance

Attacher un volume à une instance s’exécute avec le client Nova. L’instance doit être dans l’état “ACTIVE”. Nous allons ajouter une fonction qui permet à l’instance d’attendre de se trouver dans cet état.

    def wait_for_server_state(client, server, vm_state, retry, sleep):
        """Waits for server to be in vm_state

        :param client: nova client.
        :param server: server object.
        :param vm_state: for which state we are waiting for
        :param retry: how many times to retry
        :param sleep: seconds to sleep between the retries
        """

        while getattr(server, 'OS-EXT-STS:vm_state') != vm_state and retry:
            time.sleep(sleep)
            server = client.servers.get(server)
            retry -= 1
        return getattr(server, 'OS-EXT-STS:vm_state') == vm_state


    def attach_volume(server, volume):

        creds = get_credentials("nova")
        nova = n_client(version="2", **creds)

        if wait_for_server_state(nova, server, "active", 5, 10):
            return nova.volumes.create_server_volume(server.id, volume.id, None)

Redémarrer ou terminer une instance

Si vous avez un serveur objet, vous n’avez pas besoin du client Nova pour faire certaines actions :

def create_volume(size, name=None):

    creds = get_credentials("cinder")
    cinder = c_client.Client("2", **creds)

    return cinder.volumes.create(size=size, name=name)

Using these functions

Voici une fonction main() qui va utiliser les fonctions que nous avons défini plus haut afin d’implémenter un scénario très simple. Vérifiez que vous avez bien remplacé la valeur de “/path/to/my/key.pub” par le fichier contenant votre clé SSH public.

    if __name__ == "__main__":
        # Create a network and one subnet
        network = create_private_network("sample_network", ["192.168.199.0/24"])

        # Plug a router between our private network and an external network
        router = create_router("sample_router", "sample_network", "public")

        # Import a public key
        key = create_keypair("sample_key", "/path/to/my/key.pub")

        # Create security group allowing incoming ssh connections
        security_group = create_security_group("ssh", "SSH security group")

        # Launch an instance
        instance = launch_instance("api_test", "s1.cw.small-1", "Ubuntu 14.04",
                                   "sample_key", "sample_network", "ssh")

        # Create a new volume of size 1 and attach it to our instance
        volume = create_volume(size=1, name="sample-volume")
        attach_volume(instance, volume)

        # Get a floating_ip and attach it to our instance
        ip = get_or_create_floating_ip("public")
        attach_floating_ip(instance, ip)

Script complet

import os
import random
import time

from cinderclient import client as c_client
from neutronclient.v2_0 import client as q_client
from novaclient.client import Client as n_client


def get_credentials(service):
    """Returns a creds dictionary filled with the following keys:

    * username
    * password/api_key (depending on the service)
    * tenant_name/project_id (depending on the service)
    * auth_url
    * region_name

    :param service: a string indicating the name of the service
                    requesting the credentials.
    """

    creds = {}
    # Unfortunately, each of the openstack client will request slightly
    # different entries in their credentials dict.
    if service.lower() in ("nova", "cinder"):
        password = "api_key"
        tenant = "project_id"
    else:
        password = "password"
        tenant = "tenant_name"

    # The most common way to pass these info to your script is to do it through
    # your environment variables. Since we're `get`ing values from a
    # dict, it shouldn't be too difficult to set default values too.
    creds.update({
        "username": os.environ.get('OS_USERNAME', "demo"),
        password: os.environ.get("OS_PASSWORD", "password"),
        "auth_url": os.environ.get("OS_AUTH_URL",
                                    "http://192.168.0.10:5000/v2.0"),
        tenant: os.environ.get("OS_TENANT_NAME", "invisible_to_admin"),
        "region_name": os.environ.get("OS_REGION_NAME", "regionOne"),
    })

    return creds


def wait_for_server_state(client, server, vm_state, retry, sleep):
    """Waits for server to be in vm_state

    :param client: nova client.
    :param server: server object.
    :param vm_state: for which state we are waiting for
    :param retry: how many times to retry
    :param sleep: seconds to sleep between the retries
    """

    while getattr(server, 'OS-EXT-STS:vm_state') != vm_state and retry:
        time.sleep(sleep)
        server = client.servers.get(server)
        retry -= 1
    return getattr(server, 'OS-EXT-STS:vm_state') == vm_state


def create_security_group(sg_name, sg_description):
    """Create a security group

    :param sg_name: security group name.
    :param sg_description: security group description.
    """
    creds = get_credentials("nova")
    nova = n_client(version="2", **creds)

    sg_ssh = nova.security_groups.create(sg_name, sg_description)
    nova.security_group_rules.create(sg_ssh.id, ip_protocol="tcp",
                                        from_port="22", to_port="22",
                                        cidr="0.0.0.0/0")

    return sg_ssh


def create_private_network(network_name, cidr_list):
    """Create a private network and subnets. To remain readable this function
    won't do any error checking.

    :param network_name: the name of the private network
    :param cidr_list: an iterable of subnet cidr notations
    """

    credentials = get_credentials("neutron")
    neutron = q_client.Client(**credentials)

    body_sample = {'network': {'name': network_name,
                                'admin_state_up': True}}

    netw = neutron.create_network(body=body_sample)
    net_dict = netw['network']
    network_id = net_dict['id']

    # We may want to create several subnets in this network
    for cidr in cidr_list:
        body_create_subnet = {'subnets': [{'cidr': cidr,
                                            'ip_version': 4,
                                            'network_id': network_id}]}

        subnet = neutron.create_subnet(body=body_create_subnet)

    return netw


def create_router(name, private_net_name, public_net_name):
    """Creates a NAT router that will allow VMs on the private net to
    connect to the internet through the public net.

    :param name: Name the router.
    :param private_net_name: Name of the private network where our VMs
                                will be spawned.
    :param public_net_name: Name of the public network connected to
                            the internet.
    """
    credentials = get_credentials("neutron")
    neutron = q_client.Client(**credentials)

    prv_subnet_id = neutron.list_networks(
        name=private_net_name)['networks'][0]['subnets'][0]
    pub_net_id = neutron.list_networks(
        name=public_net_name)['networks'][0]['id']

    req = {
        "router": {
            "name": name,
            "external_gateway_info": {
                "network_id": pub_net_id
            },
        "admin_state_up": True
        }
    }
    router = neutron.create_router(body=req)
    router_id = router['router']['id']

    req = {
        "subnet_id": prv_subnet_id
    }
    neutron.add_interface_router(router=router_id, body=req)

    return router


def create_keypair(name, key_path):
    """This function imports a public key into nova.

    :param name: the name the keypair is imported to
    :param key_path: the path of the key file on the disk
    """
    creds = get_credentials("nova")
    nova = n_client(version="2", **creds)

    with open(key_path) as key:
        return nova.keypairs.create(name, key.read())


def launch_instance(instance_name, flavor_name, image_name, keypair_name,
                    network_label, security_group):

    creds = get_credentials("nova")
    nova = n_client(version="2", **creds)

    # we don't have the object, we only know its name. So we
    # retrieve it like this:
    image = nova.images.find(name=image_name)
    flavor = nova.flavors.find(name=flavor_name)
    network = nova.networks.find(label=network_label)

    return nova.servers.create(name=instance_name, image=image,
                                flavor=flavor, key_name=keypair_name,
                                nics=[{'net-id': network.id}],
                                security_groups=[security_group])


# First let's check if there are floating ips available. If there are, we'll
# return a random one. If not, we'll create one and return it.
def get_or_create_floating_ip(pool=None):

    creds = get_credentials("nova")
    nova = n_client(version="2", **creds)

    ip_list = nova.floating_ips.list()
    # Filtering out associated floating IPs
    ip_list = [ip for ip in ip_list if ip.instance_id is None]
    # Filtering floating IPs accord to a specific pool
    if pool is not None:
        ip_list = [ip for ip in ip_list if ip.pool == pool]

    if len(ip_list) > 0:
        # don't forget to import random
        return random.choice(ip_list)
    else:
        return nova.floating_ips.create(pool)


def attach_floating_ip(server, ip):
    return server.add_floating_ip(ip)


def create_volume(size, name=None):

    creds = get_credentials("cinder")
    cinder = c_client.Client("2", **creds)

    return cinder.volumes.create(size=size, name=name)


def attach_volume(server, volume):

    creds = get_credentials("nova")
    nova = n_client(version="2", **creds)

    if wait_for_server_state(nova, server, "active", 5, 10):
        return nova.volumes.create_server_volume(server.id, volume.id, None)


def reboot_server(server, reboot_type):
    # reboot_type can be "SOFT" or "HARD"
    server.reboot(reboot_type)


def delete_server(server):
    server.delete()

if __name__ == "__main__":
    print("Create a network and one subnet")
    network = create_private_network("sample_network", ["192.168.199.0/24"])

    print("Plug a router between our private network and an external network")
    router = create_router("sample_router", "sample_network", "public")

    print("Import a public key")
    key = create_keypair("sample_key", "/home/username/.ssh/id_rsa.pub")

    print("Create security group allowing incoming ssh connections")
    security_group = create_security_group("sample_ssh", "SSH security group")

    print("Launch an instance")
    instance = launch_instance("sample_instance", "s1.cw.small-1", "Ubuntu 16.04",
                               "sample_key", "sample_network", "sample_ssh")

    print("Create a new volume of size 1 and attach it to our instance")
    volume = create_volume(size=1, name="sample-volume")
    attach_volume(instance, volume)

    print("Get a floating_ip and attach it to our instance")
    ip = get_or_create_floating_ip("public")
    attach_floating_ip(instance, ip)