1
0
Fork 0
scripts/sysadmin/nginx/config/builder/__init__.py

210 lines
6.8 KiB
Python

"""
The Builder API defines a pluggable builder framework for manipulating nginx configs from within python.
Building a config
=================
Every config built using the builder pattern starts off with creating a :class:NginxConfigBuilder::
from nginx.config.builder import NginxConfigBuilder
nginx = NginxConfigBuilder()
By default, this comes loaded with a bunch of helpful tools to easily create routes and servers
in nginx::
with nginx.add_server() as server:
server.add_route('/foo').end()
with server.add_route('/bar') as bar:
bar.add_route('/baz')
This generates a simple config that looks like this::
error_log logs/nginx.error.log;
worker_processes auto;
daemon on;
http {
include ../conf/mime.types;
server {
server_name _;
location /foo {
}
location /bar {
location /baz {
}
}
}
}
events {
worker_connections 1024;
}
Plugins
=======
A plugin is a class that inherits from :class:`nginx.config.builder.baseplugins.Plugin` that provides
additional methods which can be chained off of the :class:`NginxConfigBuilder` object. These plugins provide
convenience methods that manipulate the underlying nginx configuration that gets built by the
:class:`NginxConfigBuilder`.
A simple plugin only needs to define what methods it's going to export::
class NoopPlugin(Plugin):
name = 'noop'
@property
def exported_methods(self):
return {'noop': self.noop}
def noop(self):
pass
This NoopPlugin provides a simple function that can be called off of a :class:`NginxConfigBuilder` that
does nothing successfully. More complex plugins can be found in :mod:`nginx.config.builder.plugins`
To use this NoopPlugin, we need to create a config builder and then register the plugin with it::
nginx = NginxConfigBuilder()
nginx.noop() # AttributeError :(
nginx.register_plugin(NoopPlugin())
nginx.noop() # it works!
A more complex plugin would actually do something, like a plugin that adds an expiry directive to
a route::
class ExpiryPlugin(Plugin):
name = 'expiry'
@property
"""
from ..api import EmptyBlock, Block, Config
from .exceptions import ConfigBuilderConflictException, ConfigBuilderException, ConfigBuilderNoSuchMethodException
from .baseplugins import RoutePlugin, ServerPlugin, Plugin
DEFAULT_PLUGINS = (RoutePlugin, ServerPlugin)
INVALID_PLUGIN_NAMES = ('top,')
class NginxConfigBuilder(object):
""" Helper that builds a working nginx configuration
Exposes a plugin-based architecture for generating nginx configurations.
"""
def __init__(self, worker_processes='auto', worker_connections=512, error_log='logs/error.log', daemon='off'):
"""
:param worker_processes str|int: number of worker processes to start with (default: auto)
:param worker_connections int: number of nginx worker connections (default: 512)
:param error_log str: path to nginx error log (default: logs/error.log)
:param daemon str: whether or not to daemonize nginx (default: on)
"""
self.plugins = []
self._top = EmptyBlock(
worker_processes=worker_processes,
error_log=error_log,
daemon=daemon,
)
self._http = Block(
'http',
include='../conf/mime.types'
)
self._cwo = self._http
self._events = Block(
'events',
worker_connections=worker_connections
)
self._methods = {}
for plugin in DEFAULT_PLUGINS:
self.register_plugin(plugin(parent=self._http))
def _validate_plugin(self, plugin):
if not isinstance(plugin, Plugin):
raise ConfigBuilderException(
"Must be a subclass of {cls}".format(cls=Plugin.__name__),
plugin=plugin
)
if plugin.name in INVALID_PLUGIN_NAMES:
raise ConfigBuilderException(
"{name} is a protected name and cannot be used as the name of"
" a plugin".format(name=plugin.name),
plugin=plugin
)
if plugin.name in (loaded.name for loaded in self.plugins):
raise ConfigBuilderConflictException(plugin=plugin, loaded_plugin=plugin, method_name='name')
methods = plugin.exported_methods.items()
# check for conflicts
for loaded_plugin in self.plugins:
for (name, method) in methods:
if name in loaded_plugin.exported_methods:
raise ConfigBuilderConflictException(
plugin=plugin,
loaded_plugin=loaded_plugin,
method_name=name
)
# Also protect register_plugin, etc.
if hasattr(self, name):
raise ConfigBuilderConflictException(
plugin=plugin,
loaded_plugin='top',
method_name=name
)
# we can only be owned once
if plugin._config_builder:
raise ConfigBuilderException("Already owned by another NginxConfigBuilder", plugin=plugin)
plugin._config_builder = self
def register_plugin(self, plugin):
""" Registers a new nginx builder plugin.
Plugins must inherit from nginx.builder.baseplugins.Plugin and not expose methods that conflict
with already loaded plugins
:param plugin nginx.builder.baseplugins.Plugin: nginx plugin to add to builder
"""
self._validate_plugin(plugin)
# insert ourselves as the config builder for plugins
plugin._config_builder = self
self.plugins.append(plugin)
self._methods.update(plugin.exported_methods)
@property
def top(self):
""" Returns the logical top of the config hierarchy.
This is a convenience method for any plugins that need to quickly access the top of the config tree.
:returns :class:`nginx.config.Block`: Top of the config block
"""
return self._http
def __getattr__(self, attr):
# Since we want this to be easy to use, we will do method lookup
# on the methods that we've gotten from our different config plugins
# whenever someone tries to call a method off of the builder.
#
# This means that plugins can just return a reference to the builder
# so that users can just chain methods off of the builder.
try:
return self._methods[attr]
except KeyError:
raise ConfigBuilderNoSuchMethodException(attr, builder=self)
def __repr__(self):
return repr(Config(self._top, self._events, self._http))