Creating Debian 8 Server with python over RESTful API

Creating Debian 8 Server with python over RESTful API

API gridscale
Creating Debian Server over python with RESTful API

Many of our examples use cURL because of their simple understanding. But much more control, better integration into existing scripts and ultimately even more performance is obtained with languages such as python.

I do not want to use this guide to build a complete library, but use the gridscale RESTful API pragmatically to create a server with python3 and the RESTful API.
Also this guide is not intended as a “benchmark”, but as a template how to use the API with python.
Therefore, the API commands deliberately sequentially and with generous polling intervals to keep the code understandable and easily reproducible.

What should the script be able to do:
– find a suitable location for a server
– create a server
– find the Debian 8 template
– create a storage
– Connect storage and public network to the server
– start the server
– Ping the server and login via SSH

Ok, let’s go 🙂

1) Script basic framework
To ensure that user ID and API tokens are not hardcoded in the script, we use argparse and pass both values at program start.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Create a server using the gridscale RESTful API.
"""
import argparse
import random
import string


def parse_args():
    """
    Setup parser for command line arguments.
    """
    apiurl = 'https://api.gridscale.io/objects'
    random_pw = ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(8))

    # create top level parser
    parser = argparse.ArgumentParser(description='Create a server using the gridscale RESTful API')

    parser.add_argument('userid',
                        type=str,
                        help='User-UUID')

    parser.add_argument('token',
                        type=str,
                        help='The API-Token (generate via https://my.gridscale.io/APIs/Create)')

    parser.add_argument('--apiurl',
                        type=str,
                        help='The API URL (default: ' + apiurl + ')',
                        default=apiurl)

    parser.add_argument('--rootpw',
                        type=str,
                        help='Server root password (default: random 8 char password)',
                        default=random_pw)

    return parser.parse_args()


def main(args):
    """
    Create a server with storage + public networking.

    1a) find a location we can use
     b) find the "Debian 8" template
     c) find the object_uuid of the public network
     d) request a new IPv4 and IPv6
    2) create a storage with the template
    3) create a server
    4) connect storage, public network, IPv4 and IPv6 to the server
    5) start the server
    """
    apiurl = args.apiurl
    headers = {'content-type': 'application/json'}
    headers['X-Auth-UserId'] = args.userid
    headers['X-Auth-Token'] = args.token


if __name__ == '__main__':
    main(parse_args())

2) Request RESTful API status
A simple function, the only purpose of which is after we submit an API request to pollen the API to completion.
This function I use to serialize everything we do with the API strictly. Of course, the API allows arbitrary, parallel requests, unless they depend on each other (for example, a storage must be created before it can be connected to a server). Threaded processing is something for one of the next guides.

import requests
import json
import time


def poll_state(apiurl, headers, url, urltype):
    """
    Basic RESTful API poller.

    Wait until a given endpoint reaches a certain "active" state.

    This helps us to serialize API requests.
    """
    poll_time_in_sec = 30
    sleep_time = 0.1
    counter = 0
    counter_max = int(poll_time_in_sec / sleep_time)

    while True:
        req = requests.get(apiurl + url, headers=headers)

        if req.status_code not in [200, 202, 204]:
            message = ['Polling failed - got status',
                       str(req.status_code),
                       'with error:',
                       str(req.text)]
            raise Exception(' '.join(message))

        data = json.loads(req.text)

        if data[urltype]['status'] in ['active']:
            break

        if counter > counter_max:
            message = ['Polling URL',
                       str(apiurl + url),
                       'timed out after',
                       str(poll_time_in_sec),
                       'seconds.']
            raise Exception(' '.join(message))
        time.sleep(sleep_time)

3) Find location, template and public network

def choose_location(apiurl, headers, name):
    req = requests.get(apiurl, headers=headers)
    locations = json.loads(req.text)['locations']

    for location in locations:
        if locations[location]['name'] == name:
            return location

    raise Exception('Location not found.')

[...]
def main(args):
[...]
    # find the uuid of 'de/fra' location
    location_uuid = choose_location(apiurl, headers, 'de/fra')
def choose_template(apiurl, headers, name):
    req = requests.get(apiurl, headers=headers)
    templates = json.loads(req.text)['templates']

    for template in templates:
        if templates[template]['name'] == name:
            return template

    raise Exception('Template not found.')

[...]
def main(args):
[...]
    # find the uuid of the "Debian 8" template
    template_uuid = choose_template(apiurl, headers, 'Debian 8 (64bit)')
def choose_public_net(apiurl, headers):
    req = requests.get(apiurl, headers=headers)
    networks = json.loads(req.text)['networks']

    for network in networks:
        if networks[network]['public_net']:
            return network

    raise Exception('Public Network not found.')

[...]
def main(args):
[...]
    # find the uuid of the public network
    public_net_uuid = choose_public_net(apiurl, headers)

4) Request an IPv4 and an IPv6 address
IP addresses are assigned directly, so no polling is necessary.
Also it can be good in a normal scenario, that one already has IPs, which one would like to use.
Then, of course, you would not request new IPs here, but with a GET on / ips similar to the locations a list of existing IPs queries and choose the right one from it.

def request_ip(apiurl, headers, ip_family, location_uuid):
    payload = {'location_uuid': location_uuid,
               'family': ip_family}

    req = requests.post(apiurl, data=json.dumps(payload), headers=headers)

    if req.status_code in [202]:
        return json.loads(req.text)

    raise Exception('IP could not be allocated')

[...]
def main(args):
[...]
    # request one IPv4 address
    ipv4_data = request_ip(apiurl, headers, 4, location_uuid)
    ipv4_uuid = ipv4_data['object_uuid']

    # request one IPv6 address
    ipv6_data = request_ip(apiurl, headers, 6, location_uuid)
    ipv6_uuid = ipv6_data['object_uuid']

5) Create storage with Debian 8 as a template

def create_storage(apiurl, headers, hostname, capacity, template_uuid, location_uuid, rootpw):
    payload = {'location_uuid': location_uuid,
               'name': 'Storage with Debian 8',
               'capacity': capacity,
               'template': {'hostname': hostname,
                            'template_uuid': template_uuid,
                            'password': rootpw,
                            'password_type': 'plain'}}

    req = requests.post(apiurl + '/storages', data=json.dumps(payload), headers=headers)

    if req.status_code not in [202]:
        raise Exception('Storage could not be created.')

    storage_data = json.loads(req.text)
    object_uuid = storage_data['object_uuid']

    poll_state(apiurl, headers, '/storages/' + str(object_uuid), 'storage')

    return object_uuid

[...]
def main(args):
[...]
    # create storage from Debian 8 template
    storage_uuid = create_storage(apiurl, headers,
                                  hostname='debian8'
                                  capacity=2,
                                  template_uuid=template_uuid,
                                  location_uuid=location_uuid,
                                  rootpw=args.rootpw)

6) Configure the server

def create_server(apiurl, headers, name, cores, memory, location_uuid):
    payload = {'location_uuid': location_uuid,
               'name': name,
               'cores': cores,
               'memory': memory}

    req = requests.post(apiurl + '/servers', data=json.dumps(payload), headers=headers)

    if req.status_code not in [202]:
        raise Exception('Server could not be created.')

    storage_data = json.loads(req.text)
    object_uuid = storage_data['object_uuid']

    poll_state(apiurl, headers, '/servers/' + str(object_uuid), 'server')

    return object_uuid
[...]
def main(args):
[...]
    # create server
    server_uuid = create_server(apiurl, headers,
                                'Debian 8 Server',
                                cores=1,
                                memory=1,
                                location_uuid=location_uuid)

7) Connect all components to the server (Storage, Public Network, IPv4, IPv6)

def add_relation(apiurl, headers, server_uuid, object_uuid, object_type):
    payload = {'object_uuid': object_uuid}

    if object_type == 'storage':
        reltype = '/storages'
    elif object_type == 'network':
        reltype = '/networks'
    elif object_type == 'ip':
        reltype = '/ips'

    request_url = apiurl + '/servers/' + str(server_uuid) + reltype
    req = requests.post(request_url, data=json.dumps(payload), headers=headers)

    if req.status_code not in [202]:
        raise Exception('Server could not be related to object.')

    poll_state(apiurl, headers, '/servers/' + str(server_uuid), 'server')

    return True

[...]
def main(args):
[...]
    # now all the server relations to
    #   - storage
    #   - public network
    #   - ipv4
    #   - ipv6
    add_relation(apiurl, headers, server_uuid, storage_uuid, 'storage')
    add_relation(apiurl, headers, server_uuid, public_net_uuid, 'network')
    add_relation(apiurl, headers, server_uuid, ipv4_uuid, 'ip')
    add_relation(apiurl, headers, server_uuid, ipv6_uuid, 'ip')

8) … and now the last step: Turn on the server

def power_on(apiurl, headers, server_uuid):
    payload = {'power': True}

    request_url = apiurl + '/servers/' + str(server_uuid) + '/power'
    req = requests.patch(request_url, data=json.dumps(payload), headers=headers)

    if req.status_code not in [202]:
        raise Exception('Server could not be started.')

    poll_state(apiurl, headers, '/servers/' + str(server_uuid), 'server')

    return True

[...]
def main(args):
[...]
    # finally power-on the server so we can use it
    power_on(apiurl, headers, server_uuid)

Done
Now run the script, measure the time for the runtime of the script, wait until the server pings and make an SSH login to the new server.
In the above examples, I omitted the console output – in the finished script below, they are included.

Run – let’s go 🙂

$ time ./create_server.py USER_ID TOKEN
Server root password: 9f3mnh8z
Choosing location...
Location found: 45ed677b-3702-4b36-be2a-a2eab9827950
Runtime was: 0.17123 seconds
---
Choosing template...
Template found: a5112c72-c9fa-4a23-8115-5f6303eb047b
Runtime was: 0.116985 seconds
---
Choosing public network...
Public network found: d20386b7-5892-447b-9e40-917d9b7a4c44
Runtime was: 0.102368 seconds
---
Requesting IPv4 ...
IPv4 found: 185.102.95.70
Runtime was: 0.203067 seconds
---
Requesting IPv6 ...
IPv6 found: 2a06:2380:0:1::26
Runtime was: 0.175562 seconds
---
Create storage...
Storage created: 7df35479-080d-4c04-9d07-3013742c4787
Runtime was: 6.554358 seconds
---
Create server...
Server created: 7cbfc4d6-4444-4222-be7d-182213c07aea
Runtime was: 0.288084 seconds
---
Server UUID: 7cbfc4d6-4444-4222-be7d-182213c07aea
Add storage to server 7cbfc4d6-4444-4222-be7d-182213c07aea
...done
Runtime was: 0.293474 seconds
---
Add network to server 7cbfc4d6-4444-4222-be7d-182213c07aea
...done
Runtime was: 0.27297 seconds
---
Add ip to server 7cbfc4d6-4444-4222-be7d-182213c07aea
...done
Runtime was: 0.314014 seconds
---
Add ip to server 7cbfc4d6-4444-4222-be7d-182213c07aea
...done
Runtime was: 0.334506 seconds
---
Power-ON Server 7cbfc4d6-4444-4222-be7d-182213c07aea
...done
Runtime was: 2.004724 seconds
---

real	0m10.997s
user	0m0.723s
sys	0m0.141s

$ ping 185.102.95.70
PING 185.102.95.70 (185.102.95.70) 56(84) bytes of data.
64 bytes from 185.102.95.70: icmp_seq=16 ttl=57 time=15.4 ms
64 bytes from 185.102.95.70: icmp_seq=17 ttl=57 time=14.7 ms
64 bytes from 185.102.95.70: icmp_seq=18 ttl=57 time=14.7 ms

$ ping6 -c 2 2a06:2380:0:1::26
PING 2a06:2380:0:1::26(2a06:2380:0:1::26) 56 data bytes
64 bytes from 2a06:2380:0:1::26: icmp_seq=1 ttl=57 time=16.0 ms
64 bytes from 2a06:2380:0:1::26: icmp_seq=2 ttl=57 time=15.8 ms

$ ssh root@185.102.95.70
Warning: Permanently added '185.102.95.70' (ECDSA) to the list of known hosts.
root@185.102.95.70's password: 

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
root@debian8:~# ping -c 2 www.google.de
PING www.google.de (173.194.113.15) 56(84) bytes of data.
64 bytes from fra02s19-in-f15.1e100.net (173.194.113.15): icmp_seq=1 ttl=60 time=0.912 ms
64 bytes from fra02s19-in-f15.1e100.net (173.194.113.15): icmp_seq=2 ttl=60 time=0.911 ms

--- www.google.de ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.911/0.911/0.912/0.030 ms
root@debian8:~#

Top – that was easy and fast. The complete script can be found here: create_server.zip

Zurück zur Tutorial Übersicht Back to Tutorial Overview

Many of our examples use cURL because of their simple understanding. But much more control, better integration into existing scripts and ultimately even more performance is obtained with languages such as python. I do not want to use this guide to build a complete library, but use the gridscale RESTful API pragmatically to create a […]

Schade, dass dir der Artikel nicht gefallen hat.
Was sollten wir deiner Meinung nach besser machen?

Vielen Dank für dein Feedback!
Wir melden uns bei dir, sobald der Artikel zu deinem Wunschthema fertig ist.

Übrigens: kennst du schon unser Tutorial zum Thema Resize Storage on the fly – without rebooting your server?