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)