Ny konfigurasjon i Cerebrum

Det har blitt implementert et konfigurasjonsrammeverk i Cerebrum. Dette dokumentet beskriver hvordan dette bør taes i bruk.

Innføring

Oversikt

Formkrav til konfigurasjon

All konfigurasjon må nå _implementeres_ i en egen klasse. Eksempel:

class MyConfig(Configuration):
    host = ConfigDescriptor(String)
    port = ConfigDescriptor(Integer)

Dette gjør at konfigurasjon og kode henger sammen i versjonskontroll. Det at vi har _ett_ konfigurasjonsobjekt gjør også at vi setter litt formkrav til konfigurasajon. Mer om dette under kodestandard.

Gruppering

Innstillinger kan grupperes i _namespaces_ (egne konfigurasjoner). Namespaces kan gjenbrukes (f.eks. flere steder i samme konfigurasjon, eller på tvers av konfigurasjon. Eksempel:

class WebAddress(Configuration):
    url = ConfigDescriptor(String, default="http://localhost")
    port = ConfigDescriptor(Integer, minval=0, maxval=65535, default=80)

class MyConfig(Configuration):
    my_client = ConfigDescriptor(Namespace, config=WebAddress)
    your_client = ConfigDescriptor(Namespace, config=WebAddress)

Dette betyr at vi kan implementere konfigurasjon for en gitt modul i selve modulen. Konfigurasjonsobjektet kan så taes i bruk der som modulen benyttes.

Gjentatte konfigurasjonsmønster kan implementeres generelt, og gjenbrukes på tvers av moduler. F.eks. kan det f.eks. implementeres _en_ konfigurasjon for requests-pakken, som videre kan benyttes av alle moduler som benytter denne pakken for å gjøre HTTP-requests.

Hvis vi da implementerer støtte for å konfigurere _proxy_ for requests, vil vi kunne benytte proxy-instillinger alle steder hvor requests benyttes.

Dokumentasjon

Alle innstillinger kan dokumenteres i kode. Dokumentasjon kan hentes ut på maskinlesbart og menneskelesbart format. Eksempel:

class WebAddress(Configuration):

    url = ConfigDescriptor(
        String,
        default="http://localhost",
        doc="Url to the service (without port)")

    port = ConfigDescriptor(
        Integer,
        default=80,
        doc="Port for the service.")

class MyConfig(Configuration):

    my_client = ConfigDescriptor(
        Namespace,
        config=WebAddress,
        doc="Connection config for my_client")

    your_client = ConfigDescriptor(
        Namespace,
        config=WebAddress,
        doc="Connection config for your_client")

print MyConfig.documentation()

Vil da gi output:

<class 'MyConfig'>
    my_client: <class 'Cerebrum.config.configuration.Namespace'>
        port: <class 'Cerebrum.config.settings.Integer'>
            description: Port for the service.
            default: 80
            types: (<type 'int'>, <type 'long'>)
        url: <class 'Cerebrum.config.settings.String'>
            description: Url to the service (without port)
            default: 'http://localhost'
            types: (<type 'str'>, <type 'unicode'>)
    your_client: <class 'Cerebrum.config.configuration.Namespace'>
        port: <class 'Cerebrum.config.settings.Integer'>
            description: Port for the service.
            default: 80
            types: (<type 'int'>, <type 'long'>)
        url: <class 'Cerebrum.config.settings.String'>
            description: Url to the service (without port)
            default: 'http://localhost'
            types: (<type 'str'>, <type 'unicode'>)

Her er det noe rom for forbedring i selve formatteringen. Det kan også implementeres nye formatterere som kan generere standardiserte, maskinlesbare schemas, f.eks. jsonschema.

Konfigurasjon kan utvides

Konfigurasjon kan utvides dynamisk, så lenge det skjer før konfigurasjonsobjekt taes i bruk. Eksempel:

class MyConfig(Configuration):
    foo = ConfigDescriptor(String)

MyConfig.bar = ConfigDescriptor(Integer, default=1)

Dette gjør at vi på sikt kan ha ett globalt konfigurasjonsobjekt for Cerebrum, som bygges ut i fra hvilke _moduler_ som er tatt i bruk.

Konfigurasjon i kode

Konfigurasjonsklasser kan brukes som en del av kode. Eksempel:

class MyClient(SomeClient, Configuration):
    url = ConfigDescriptor(String, default="http://localhost")
    port = ConfigDescriptor(Integer, minval=0, maxval=65535, default=80)

    def __init__(self, url, port):
        super(MyClient, self).__init__()
        self.url = url
        self.port = port


client = MyClient(8080, 'localhost')

# TypeError: Invalid type <type 'int'> for setting
#   <class 'Cerebrum.config.settings.String'>,
#   must be (one of): (<type 'str'>, <type 'unicode'>)
Filformat

Filformat for konfigurasjon er løsknyttet fra rammeverket. Alle filformat som støtter tall, strenger, lister og mapping kan implementeres og kobles på konfigurasjonsrammeverket.

Støtte for JSON og YAML er implementert.

Innlesing av konfigurasjon

Innlesing av konfigurasjon skjer fra forhåndsdefinerte steder. I tillegg kan man lett overstyre konfigurasjon, og konfigurasjon kan deles opp i flere filer, eller samles i en enkelt fil

Gitt følgende filer:

  • /etc/myproduct/services.yml

    my_client:
        url: 'http://localhost'
        port: 80
    your_client:
        url: 'http://localhost'
        port: 8080
    
  • ~/.myproduct/configs/services.json

    {"my_client.port": 8090", "your_client.port": 8091}
    
  • some_folder/your_client.yml

    url: 'https://test.example.org'
    

og

conf = MyConfig()
loader.default_dir = '/etc/myproduct'
loader.user_dir = '~/myproduct/configs
loader.read(conf, root_ns='services', additional_dirs=['some_folder', ])
print repr(config)

Vil man få:

my_client:
    url: 'http://localhost'
    port: 8090
your_client:
    url: 'https://test.example.org'
    port: 8091

Dette gjør at vi kan organisere store navnerom i egne konfigurasjonsfiler, samt at man kan overstyre konfigurasjon enkelt i f.eks. testmiljøer.

Validering

Alle innstillinger vil kunne valideres, og det kan legges inn begrensninger for gyldige verdier. F.eks.: Tall mellom 5 og 10, liste med maks 5 strenger som matcher en gitt regex. Eksempel:

class WebAddress(Configuration):

    url = ConfigDescriptor(
        String,
        default="http://localhost",
        regex="^https?://'
        doc="Url to the service (without port)")

    port = ConfigDescriptor(
        Integer,
        default=80,
        minval=0,
        maxval=65535,
        doc="Port for the service.")

Begrensningene vil sjekkes når verdien endres, og blir tatt med i output for dokumentasjon.

Dette gjør at vi kan fange opp konfigurasjonsfeil tidlig.

Identifisering av feil

Man vil få beskjed om hvor en feil finnes ved innlesing av konfigurasjon. Eksempel:

config = MyConfig()
config.load_dict({
    'my_client.url': 'localhost',
    'my_client.port': -1, })

# Cerebrum.config.errors.ConfigurationError: Errors in
#     'my_client.port' (ValueError: Invalid value -1, must not be less than 0),
#     'my_client.url' (ValueError: Invalid value 'localhost', must pass regex '^https?://')


config = MyConfig()
config.load_dict({'my_client': {'url': 'http://localhost', 'port': 80, }, })
config.validate()

# Cerebrum.config.errors.ConfigurationError: Errors in
#   'your_client.port' (TypeError: Invalid type <class 'Cerebrum.config.settings.NotSetType'> for setting… ),
#   'your_client.url' (TypeError: Invalid type <class 'Cerebrum.config.settings.NotSetType'> for setting… )

Her er det noe rom for forbedring mtp. innlesning av konfigurasjon som er spredt over flere filer, samt selve feilmeldingene.

Kodestandard

Gjenstående arbeid

cerebrum_path

Behovet for cerebrum_path er ikke lenger tilstede når vi går bort fra å bruke python-moduler som konfigurasjon. Man kan kanskje si at behovet aldri har vært der, da man enkelt kan komme rundt dette ved å inkludere konfigurasjonsmappe i PYTHONPATH.

cerebrum_path har likevel noe nytteverdi i at den kan brukes for generell bootstrap-kode for Cerebrum. Dette behovet kan også løses på andre (bedre) måter.

Alternativer

Formålet med cerebrum_path er å kjøre bootstrap-kode før et script kjører. Tanken bak dette var hovedsaklig å legge konfigurasjonsmapper inn i sys.path, slik at f.eks. cereconf kunne importeres. For at dette skal fungere, må script alltid importere modulen cerebrum_path.

Dette har to problemer:

  • Dersom scripts eller moduler ikke importerer cerebrum_path, så vil heller ikke bootstrap kjøres.
  • Dersom et script importerer cerebrum_path, men modulen ikke eksisterer, så vil ikke script fungere.

Det finnes flere, bedre alternativ for å kjøre bootstrap-kode, uten å bruke en modul som må importeres

Bruke pythons innebygde site-modul

Bootstrap-kode kan legges i en sitecustomize-modul i brukerens eller systemets site-packages.

::
import site print site.getsitepackages() print site.getusersitepackages()

sitecustomize har den fordelen at man ikke eksplisitt trenger å importere den. I tillegg kan funksjonaliteten enkelt slåes av:

python -s:
    Don't add user site directory to sys.path

python -S:
    Disable the import of the module site and the site-dependent
    manipulations of sys.path that it entails.

PYTHONNOUSERSITE
    If this is set to a non-empty string it is equivalent to specifying the
    -s  option

Skrive launcher-modul

Alternativt kan man skrive en egen modul for å bootstrappe kode før scripts eller tjenester kjøres.

Dette har den fordelen at bootstrap-scripts ikke må legges inn som en modul i en site-katalog.

inject/__main__.py:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
u""" Simple module for running boostrap scripts.

Usage
-----
python -m inject  script.py --logger-name=console
python -m inject --boostrap cerebrum_path.py  script.py --logger-name=console

"""
import sys
import os

bootstrap_files = list()

while True:
    try:
        argv_pos = sys.argv.index('--bootstrap')
        sys.argv.pop(argv_pos)
        bootstrap_files.append(sys.argv.pop(argv_pos))
    except ValueError:
        break
    except IndexError:
        raise SystemExit(u'The `--bootstrap` option requires an argument')

sys.argv.pop(0)

try:
    script = sys.argv[0]
except IndexError:
    raise SystemExit(u"Script argument required.")

for f in bootstrap_files:
    execfile(f)
execfile(script)
Publisert 10. feb. 2016 13:58