First Steps With The OpenStack SDK

Preface

The OpenStack SDK is a OpenStack project aimed at providing a complete software development kit for the programs which make up the OpenStack community. It is a Python library with corresponding documentation, examples, and tools released under the Apache 2 license.

OpenStackSDK Docs: About the Project

The OpenStack SDK aims to replace use of individual OpenStack client libraries (e.g. python-glanceclient, python-neutronclient, python-novaclient etc) for controlling OpenStack clouds via code, and as I understand it, is considered the preferred means of programmatically controlling OpenStack in new projects.

Since it should theoretically make life easier for doing Infrastructure via Code, this is approach which which this learning journey has begun.

The scripts described on this page are available in a Git repo @ https://github.com/danielfdickinson/ivc-in-the-wtg-experiments

Prerequisite Config

Aside from installing the OpenStack SDK itself, it is necessary to create a configuration. The chosen approach was to use clouds.yaml in the same directory as the scripts.

clouds.yaml

clouds:
  ovh:
    profile: ovh
    auth:
      auth_url: https://auth.cloud.ovh.net/v3/
      username: 'user-XXXXXXXXXXXXXXXX'
      user_domain_id: default
      tenant_name: 'a_redacted_tenant_name'
      project_domain_id: default
      project_id: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    region_name: FAK1
    interface: public
    identity_api_version: 3

One Step Backward, Two Steps Forward

A first attempt at using the OpenStackSDK ran into a bug, but two subsequent experiments went well.

The OpenStack SDK Isn’t Perfect

The first project attempted was to generate and use a token to list servers currently running on an OpenStack Project. This ran into LP#1437976. As you can this isn’t a problem with the sdk per se, rather the issue occurs in an underlying library.

The issue occurs with tokens created using openstack token issue as well as the following programmatic approach:

get-os-compute-token.py

from getpass import getpass
import openstack

os_pass = getpass("Enter your password: ")

conn = openstack.connect(cloud='ovh', password=os_pass)
print("Your token is: ")
print(conn.authorize())

The failing code to list existing servers (virtual machines), with token: the_token instead of username: a_user_name in the clouds.yaml:

os-list-instances.py

import openstack

conn = openstack.connect(cloud='ovh')

servers = conn.list_servers()

for server in servers:
    print(server.name)

Using a Username and Password Worked

Removing the token: the_token and using the following code instead worked, as did using the above script with a username and password in clouds.yaml (It is better not to keep credentials lying around in plaintext, hence the following):

os-list-instances.py

import openstack
from getpass import getpass

username = input("Enter your username: ")
password = getpass("Enter your password: ")

conn = openstack.connect(cloud='ovh', username=username, password=password)

servers = conn.list_servers()

for server in servers:
    print(server.name)

Creating a Basic Instance

The next experiment was a very basic instance creation, with a preferred username and provisioning SSH key and password (and no passwordless sudo).

For that the following script was created, with relative ease:

create-basic-instance.py

import sys
import openstack
from getpass import getpass

username = input("Enter your username: ")
password = getpass("Enter your password: ")
server_name = input("Enter a name for your server: ")

conn = openstack.connect(cloud='ovh', username=username, password=password)

image = conn.get_image("user-base-0.3.2")
flavor = conn.get_flavor("d2-2")
network = conn.get_network("Ext-Net")

server = conn.get_server(server_name, bare=True)

userdata = """
#cloud-config
disable_root: true
preserve_hostname: false
manage_etc_hosts: true
ssh_pwauth: false
users:
  - name: anadmin
    groups:
      - adm
      - staff
      - sudo
    homedir: /local-home/anadmin
    lock_passwd: false
    hashed_passwd: $6$rounds=4096$Uai52ED7FnpSxVd1$iY6tuSJ2dpm1Owa41NUSvp/H1M39ZnVjiP9OWK3r9I/mm4lV.vaHlUodCWQOUGv9paOHZa8gh/9MX4.It6cAH/
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC43t3iUh8uqWzajviRlEtIrrVPyEHHNFG2/Ne57/CwLq2ptqCN/VEG9OAmlkMTYkZU5AMAtpe3HYl1YsCgNLdxZlmaHVffGMPwaUxEYOtqyOMhqjJr95S8cfnZ/uQ6to+HwEPpxA3GOEURXU5ti5mpecwI2YKA4mvHwYjkDKq+bnLtSTg/+iQcTC/kX0efU4VIZ5tSDHiL8DuTzpS+g7euqLSaABQjMGJ0819bPs8zc/NP1Vx3oql2QrUuTrWcneYSqn71VvCIpjeAJ0j9PHWmlcL3rdc9NF0sk7ZCixhKvvHRdmCWWUTJ0Aaw66T7By+a3wwlhcY2/tpQHJltGBWVTRQS0eMit18DCOr5oLgf7xAQkZrWXCIRbbgxBY+hOFITkXM4vXyVfzYZ0WiBATp0UtKor+SR3MPcXkX8t+ok4IWvlxmB81E+NOFyKyw1ysDMwLPtOy2YiOdIWIWkPe6M2ih0BDPGxx5fSig7819Jo99gsrU6gc02zR8OUCuWy/M= demo@keygen
timezone: America/Toronto
write_files:
  - path: /etc/byobu/autolaunch
  - path: /etc/ssh/sshd_config.d/pubkey_only.conf
    content: |
      PasswordAuthentication no
      PubkeyAuthentication yes
  - path: /etc/ssh/sshd_config.d/alternate_port.conf
    content: |
      Port 27221
"""

if server is not None:
    yesno = input("Server already exists; delete it? (Must enter 'yes' in all caps, followed by ENTER key): ")
    if yesno != "YES":
        print("Bailing...")
        sys.exit(1)

    yesno = input("I know you hate this, but are you really sure? (Must enter 'YES' all lower case, followed by ENTER key): ")
    if yesno != 'yes':
        print("Bailing...")
        sys.exit(1)
    
    if conn.delete_server(server, True, 600):
        print("Server successfully deleted")
    else:
        print("Weird. Server doesn't exist. Bailing...")
        sys.exit(2)

server = conn.create_server(server_name, image=image, flavor=flavor, network=network, userdata=userdata, wait=True, timeout=600)

print("ip: {ip}".format(ip=server.accessIPv4))

A More Complete Basic Instance Creation Script

This script introduces basic usage of the configparser to configure the creation of a set of instances, as well as introducing logic to better handle missing or not found resources, missing configuration, etc.

The userdata was moved unchanged into a file called userdata-basic-instance.yaml, and a config file called basic-instance.ini was created.

basic-instance.ini

[DEFAULT]
cloud = ovh
username = user-XXXXXXXXXXXX
image = user-base-0.3.2
flavor = d2-2
network = Ext-Net

[server1]
server_name = pytest001

[server2]
server_name = pytest002
delete_if_exists = yes

create-basic-instances-from-config.py

import sys
import openstack
from getpass import getpass
import configparser

passwords = {}

config = configparser.ConfigParser(
    defaults={
        "delete_if_exists": "no",
        "remember_password": "yes",
        "userdata": "userdata-basic-instance.yaml",
    }
)

readfile = config.read("basic-instance.ini")

if len(readfile) < 1:
    print("Failed to read config file. Bailing...", file=sys.stderr)
    sys.exit(1)

print("Creating {num_servers} servers".format(num_servers=(len(config.sections()))))

for section in config.sections():
    sectmap = config[section]
    username = sectmap["username"]
    remember_password = sectmap.getboolean("remember_password")
    delete_if_exists = sectmap.getboolean("delete_if_exists")

    password = passwords.get(username)
    if password is None:
        password = getpass(
            "Enter your password for user {username}: ".format(username=username)
        )
        if remember_password:
            passwords[username] = password

    conn = openstack.connect(
        cloud=sectmap["cloud"], username=username, password=password
    )
    server = conn.get_server(sectmap["server_name"], bare=True)

    image = conn.get_image(sectmap["image"])
    if image is None:
        print(
            "Unable to find image {image}. Bailing...".format(image=sectmap["image"]),
            file=sys.stderr,
        )
        sys.exit(1)
    flavor = conn.get_flavor(sectmap["flavor"])
    if flavor is None:
        print(
            "Unable to find flavor {flavor}. Bailing...".format(
                flavor=sectmap["flavor"]
            ),
            file=sys.stderr,
        )
        sys.exit(1)
    network = conn.get_network(sectmap["network"])
    if network is None:
        print(
            "Unable to find network {network}. Bailing...".format(
                image=sectmap["network"]
            ),
            file=sys.stderr,
        )
        sys.exit(1)

    userdatafile = open(sectmap["userdata"], "r")
    userdata = userdatafile.read()
    userdatafile.close()

    if server is not None:
        if delete_if_exists:
            if conn.delete_server(server, True, 600):
                print("Server {server_name} successfully deleted".format(
                    server_name=sectmap["server_name"]
                ))
            else:
                print(
                    "Weird. Server {server_name} doesn't exist after all. Bailing...".format(
                        server_name=sectmap["server_name"]
                    ),
                    file=sys.stderr,
                )
                sys.exit(1)
        else:
            print(
                "Server {server_name} exists and delete_if_exists is False. Skipping to next server".format(
                    server_name=sectmap["server_name"]
            ))
            continue

    server = conn.create_server(
        sectmap['server_name'],
        image=image,
        flavor=flavor,
        network=network,
        userdata=userdata,
        wait=True,
        timeout=600,
    )

    if server is not None:
        print("Created server {server_name} with ip {ip}.".format(server_name=sectmap['server_name'], ip=server.accessIPv4))

Conclusion

So far, aside from the token issue, using the OpenStack SDK and Python to create instances has been quite easy, and there has been sufficient (and correct) documentation of the Connection object which so far has been able to readily meet the requests that have been needed to be made.

There were of course the usual typos and bloopers, but those were easily solved.

Overall the OpenStack SDK looks like it will be able to provide all the functionality this ‘Infrastructure via Code’ project will require.

Next Steps with OpenStack SDK

  • Adding additional instance creation details like security groups, private networks (when needed), and attaching existing volumes
  • Using Jinja to create ‘userdata’ base templates and user templates
  • More complex userdata scenarios

In short creating a sufficiently complete scripting environment to manage the desired OpenStack infrastructure.