Serveradmin Documentation¶
Extending Serveradmin¶
Serveradmin is a Django application. General knowledge about running Django applications would be useful.
Running Serveradmin¶
We provide a docker-compose setup that gives you a local development instance with 2 commands.
First make sure you have docker-compose installed as described here.
Then run these two commands:
cp .env.dist .env
docker-compose up
The default values in .env.dist are sufficient however feel free to adjust them to your needs.
You can access the web service to execute Django commands and run scripts:
docker-compose exec web
# Example: Run Django management commands
pipenv run python -m serveradmin -h
# Example: Use the Python Remote API
pipenv run python -m adminapi "hostname=example.com"
Tip
You may still want to have a virtual environment for Serveradmin on your host machines and run pipenv install -D to have all modules available for your IDEs auto completion etc.
Database Dump¶
If you have a running instance of Serveradmin which is reachable via SSH you can update the PRODUCTION_DB variable in .env to your database host and run dump.sh from your host machine:
# .env
PRODUCTION_DB=your-serveradmin-db-host.example.com
# Execute on host
./dump.sh
Testing your changes¶
We have some tests which are executed when making a PR that you can already run locally to check if your changes are breaking anything existing. They are far from comprehensive at the time of writing this but can safe you some manual testing.
You can execute the tests with the following commands
# Tests for the commandline interface: adminapi pipenv run python -m unittest discover adminapi -v
# Tests for the backend code pipenv run python -Wall -m serveradmin test –noinput –parallel
Bonus: Setting up a cool debugger¶
Install django-extensions
and werkzeug
using pip:
pip install django-extensions werkzeug
and add 'django_extensions'
to your INSTALLED_APPS
setting in the
local_settings.py
.
Now you can use python -m serveradmin runserver_plus
to start the local
test webserver with the Werkzeug debugger.
See http://packages.python.org/django-extensions/ for details.
Code style guideline¶
First of all, read the Python style guide (PEP 8). The most important things:
Use 4 spaces for indention, not tabs
Functions and variables use underscores (e.g.
config_dir
)Classes use CamelCase (e.g.
NagiosCommit
)Try to keep lines less than 80 chars
Warning
Ignoring the style guide will make your local Python expert quite sad!
Terminology¶
Just to have same names:
- project:
Many applications together with settings, a global
urls.py
and the__main__.py
form a project. The “serveradmin” is a project.- application (or “app”):
An application is basically a combination of several files for the same topic. You may have an application for nagios, graphs, the servershell etc. Applications consist of views, models and templates. If you are familiar with MVC pattern, think of views being the controllers and the templates the views.
- models:
The models will contain your application logic. This is mostly your database structure and operations on on it, but also stuff that’s not related to the database. In your application you will find a
models.py
where you can put your code in. Django calls a class inheritingdjango.db.models.Model
a model, which should not be mistaken for the models itself (e.g. a class for your database table and operations vs. your application logic in general)- views:
The views will get the input from the user and ask the model for the execution of operations or fetch data from the model to pass it to the template. As already said, it’s known as the controller in the MVC pattern. You will add your view functions to the
views.py
in your application.- templates:
The template is - in most cases - just an ordinary HTML file with some template markup to display the data it got from the view. They usually reside in a directory named
yourapp/templates/yourapp
. You have to create it yourself for a new application.
Short git introduction¶
Set your name and email:
git config --global user.name "Your Name"
git config --global user.email your.name@innogames.de
Fetch new changes from remote repository:
git pull
For changes create a new branch, and switch to it:
git branch my_changes
git checkout my_changes
Do your code changes and don’t forget to commit often. It’s good to commit even small changes. Before you commit, you have to add files (even just modified files):
git add new_file
git add file_you_have_modified
git commit
Don’t forget to put a meaningful commit message.
Once you have done all your changes and your version is ready for deployment you can merge it back to main. You may want to fetch changes from remote first:
git checkout main
git pull # Optionally fetch changes from remote
git merge my_changes
After merging was successful, you can delete your branch:
git branch -d my_changes
It is recommended to do a rebase. This will help to have a clear history:
git rebase
And finally push your changes to the remote repository:
git push
Have any changes you don’t want to commit and still want to change branch? Use git stash:
git stash # Will save your uncomitted changes
# Do whatever you want (e.g. changing branches)
git stash pop # Will apply changes again and pop it from stash
Short Django introduction¶
If you have some time I recommend doing the Django Tutorial. It covers many topics and gives your a good overview.
For people in a hurry: You will find the Serveradmin in the serveradmin
directory while the Remote API (aka. adminapi) is inside adminapi
. We will
only cover the Serveradmin in this document.
Inside the serveradmin you will find the following files:
urls.py
settings.py
The settings.py
contains your settings. You have already edited this file.
Inside the urls.py
you can define URLs for the Serveradmin. In most cases
you will have an own urls.py
in your application.
We will create a small example application named “secinfo” (for “security information”). Please don’t commit this application, it is for learning purposes only!
We will use python -m serveradmin
to create our application:
python -m serveradmin startapp secinfo
Now we have a directory named secinfo
with some files inside it. We will
move it into the directory serveradmin
.
Adding functions to the remote API¶
To create new functions which are callable by the Python remote API you have
to define them inside the api.py
file in your application. If it doesn’t
exist, you can just create it.
To export the function you will use the api_function
decorator, as shown
in the following example:
from serveradmin.api.decorators import api_function
@api_function(group='example')
def hello(name):
return 'Hello {0}!'.format(name)
Now you can call this function remotely:
from adminapi import api
example = api.get('example')
print example.hello('world') # will print 'Hello world!'
The API uses JSON for communication, therefore you can only return and receive
a restricted set of types. The following types are supported: string, integer,
float, bool, dict, list and None. You can also receive and return datetime/date
objects, but they will be converted to an unix timestamp prior sending. You have
to convert them back manually by using datetime.fromtimestamp
.
It has also limited support for exceptions. You can either raise a ValueError
if you get invalid parameters or use serveradmin.api.ApiError
for other
exceptions. You can subclass ApiError
for more specific exceptions.
Raising exception has also one other restriction: you can only pass a message,
but not additional attributes on the exception.
Look at the following example:
from serveradmin.api.decorators import api_function
from serveradmin.api import ApiError
@api_function(group='example')
def nagios_downtimes(from_time, to_time):
if to_time < from_time:
raise ValueError('From must be smaller than to')
try:
return get_nagios_downtimes(from_time, to_time)
except NagiosError, e:
# Propagating NagiosError would raise an exception in the
# serveradmin, but not on the remote side. You have to catch
# it and reraise it as ApiError or subclass of ApiError
raise ApiError(e.message)
Handling Permissions¶
We will use Django’s integrated Permission system. In Django, you will define
permissions on a model. You will automatically get a few magic permissions
named app_label.(add|change|delete)_modelname
. For example: if you have
a class Bird
in your application bird
you will get permissions
named bird.add_bird
etc. If you need own permissions, you have to
define them like this:
class Bird(models.Model):
# Fields left out
class Meta:
permissions = (
('can_fly', 'Can fly'),
)
You will now get a permission named bird.can_fly
.
If you don’t have a model class you have to create one. This will normally
also create a database table, but you can avoid it by setting managed
to False
. This will tell Django that it shouldn’t manage the database
for this model. See the following example:
class ddosmanager (models.Model):
class Meta:
managed = False
permissions = (
('set_state', 'Can enable and disable DDoS Mitigation'),
('set_prefixes', 'Can modify prefixes announced to DDoS Mitigation provider'),
('view', 'Can view DDoS Mitigation state and prefixes'),
)
There are several ways to check for permissions at different levels. To check
permissions on a view, use the permission_required
decorator:
from django.contrib.auth.decorators import permission_required
@permission_required('can_view_graphs')
def view_graphs(request):
pass # Do some stuff and render template
It will disallow calling this view for all users that don’t have the required permission.
To check permissions in the template you can use the perms
proxy. Look at
the following example:
{% if perms.bird.add_bird %}
<a href="{% url bird_add %}">Add a bird</a>
{% endif %}
Warning
Just hiding things it the template might not be enough. For example you should not hide a form, but leave the view with form processing unchecked.
In the code permissions can be checked using the user.has_perm
method. See
the following example in a view:
def change_bird(request, name):
bird = get_object_or_404(Bird, pk=range_id)
if request.method == 'POST':
can_delete = request.user.has_perm('bird.delete_bird')
can_edit = request.user.has_perm('bird.change_bird')
if action == 'delete' and can_delete:
bird.delete()
if action == 'edit' and can_edit:
pass # edit ip range
To grant permissions to users, use the Django admin interface. Superusers will have all permissions be default.
See the Django documentation on permissions for details.
Python Remote API¶
The Adminapi provides a python module which can talk to the Serveradmin via an API. It provides functions for querying servers, modifying their attributes, triggering actions (e.g. committing nagios) etc.
Warning
This is only a draft. The API might change.
Authentication¶
Every script that uses the module must authorize itself before using the API. You need to generate a so called application for every script in the admin interface of the serveradmin. This has several benefits over using a generic password:
Logging changes that were done by a specific script
Providing a list with existing scripts which are using the API
Possibility to revoke an authentication token without changing every script
The API allows authentication of an application either via public-key cryptography (ssh-keys) or pre shared keys (passwords). Using the new public-key style authentication has even more benefits:
An application can have multiple public keys, making it easier to change them
Adminapi can sign requests via keys in your local or forwarded ssh-agent
Keys in the ssh-agent can be password protected on disk and decrypted only inside the agent. Adminapi never even sees the private part of the key
Serveradmin only knows the public part of the key, while an admin can read all pre shared keys from serveradmin and use them to impersonate others.
To authenticate yourself via an ssh key you have to add the public part to an application in serveradmin. You can then either add the private key to your ssh-agent or export the SERVERADMIN_KEY_PATH environment variable to the path of the private key:
# Use ssh-agent, passwords protected keys are supported
ssh-add ~/.ssh/id_rsa
# Use environment vairable, passwords protected keys are _not_ supported
export SERVERADMIN_KEY_PATH=~/.ssh/id_rsa
Note that for ed25519 key support both adminapi and serveradmin must have paramiko 2.2 or newer installed.
To authenticate yourself via a pre shared key you need to set the SERVERADMIN_TOKEN environment variable or create a file called .adminapi in the home folder of your user:
# Use environment variable (Useful for transient jobs such as Jenkins)
export SERVERADMIN_TOKEN=MLifIK9FMQTaFDneDneNg30pb
# Use .adminapirc file in home folder
echo "auth_token=MLifIK9FMQTaFDneDneNg30pb" >> ~/.adminapirc
chmod 0600 ~/.adminapirc
The order of prevalence is:
SERVERADMIN_KEY_PATH if set
SERVERADMIN_TOKEN if set
~/.adminapirc if present
ssh-agent if present
Note that we try to authenticate with all keys in the agent. If multiple keys, belonging to different applications, match you will get a permission denied. This is because the associated apps likely have different permissions and we don’t want to guess which to enforce. Trying to authenticate with more than 20 keys will also be denied to prevent a DOS.
Querying and modifying servers¶
Using the dataset
module you can filter servers by given criteria and
modify their attributes.
Basic queries¶
You can use the adminapi.dataset.Query
function to find servers which
match certain criteria. See the following example which will find all
webservers of Tribal Wars:
from adminapi.dataset import Query
hosts = Query({'servertype': 'vm', 'game_function': 'web'})
for host in hosts:
print(host['hostname'])
The Query class takes keyword arguments which contain the filter conditions. Each key is an attribute of the server while the value is the value that must match. You can either use strings, integers or booleans for exact value matching. All filter conditions will be ANDed.
More often you need filtering with more complex conditions, for example regular expression matching, comparison (less than, greater than) etc. For this kind of queries there is a filters modules which defines some filters you can use. The following example will give you all Tribal Wars webservers, which world number is between 20 and 30:
from adminapi.filters import All, GreaterThan, LessThan
hosts = Query({
'servertype': 'vm',
'game_function': 'web',
'game_world': All(GreaterThan(20), LessThan(30)),
})
Accessing and modifying attributes¶
Each server is represented by a server object which allows a dictionary-like
access to their attributes. This means you will have the usual behaviour of
a dictionary with methods like keys()
, values()
, update(...)
etc.
You can get server objects by iterating over a query or by calling
get()
on the query. Changes to the attributes are not directly
committed. To commit them you must call commit()
on the query.
Here is an example which cancels all servers for Seven Lands:
hosts = Query({'servertype': 'hardware'}, ['canceled'])
for host in hosts:
hosts['canceled'] = True
hosts.commit()
Another example will print all attributes of VM objects and check for the
existence of the function
attribute:
vm = Query().new_object('vm')
for attr, val in vm.items():
print('{} => {}'.format(attr, val))
if 'function' not in techerror:
print('Something is wrong!')'
Multi attributes are stored as instances of adminapi.dataset.MultiAttr
,
which is a subclass of set. Take a look at set
for the available
methods. See the following example which iterates over all additional IPs and
adds another one:
techerror = Query({'hostname': 'techerror.support.ig.local'}, ['additional_ips']).get()
for ip in techerror['additional_ips']:
print(ip)
techerror['additional_ips'].add('127.0.0.1')
Warning
Modifying attributes of a server object that is marked for deleting will
raise an exception. The update()
function will skip servers that
are marked for deletion.
Query Reference¶
The adminapi.dataset.Query
function returns a query object that
supports iteration and some additional methods.
- class Query¶
- __iter__()¶
Return an iterator that can be used to iterate over the query. The result itself is cached, iterating several times will not hit thedatabase again. You usually don’t call this function directly, but use the class’ object in a for-loop.
- __len__()¶
Return the number of servers that where returned. This will fetch all results.
- get()¶
Return the first server in the query, but only if there is just one server in the query. Otherwise, you will get an exception. #FIXME: Decide kind of exception
- commit_state()¶
Return the state of the object.
- commit()¶
Commit the changes that were done by modifying the attributes of servers in the query. Please note: This will only affect servers that were accessed through this query!
- rollback()¶
Rollback all changes on all servers in the query. If the server is marked for deletion, this will be undone too.
- delete()¶
Marks all server in the query for deletion. You need to commit to execute the deletion.
Warning
This is a weapon of mass destruction. Test your script carefully before using this method!
- update(**attrs)¶
Mass update for all servers in the query using keyword args. Example: You want to cancel all Seven Land servers:
Query({'servertype': 'hardware'}).update(canceled=True)
This method will skip servers that are marked for deletion.
You still have to commit this change.
Server object reference¶
The reference will only include the additional methods of the server object.
For documentation of the dictionary-like access see dict
.
- class DatasetObject¶
- old_values¶
Dictionary which contains the values of the attributes before they were changed.
- is_dirty()¶
Return True, if the server object has uncomitted changes, False otherwise.
- is_deleted()¶
Return True, if the server object is marked for deletion.
- delete()¶
Mark the server for deletion. You need to commit to delete it.
Making API calls¶
API calls are split into several groups. To call a method you need to get a group object first. See the following example for getting a free IP:
# Do authentication first as described in section "Authentication"
from adminapi import api
nagios = api.get('nagios')
nagios.commit('push', 'john.doe', project='techerror')
Adminapi Module Documentation¶
- class adminapi.dataset.Query(filters=None, restrict=['hostname'], order_by=None)¶
- class adminapi.dataset.MultiAttr(other, obj, attribute_id)¶
This class must redefine all mutable methods of the set class to maintain the old values on the DatasetObject.
- add(elem)¶
Add an element to a set.
This has no effect if the element is already present.
- clear()¶
Remove all elements from this set.
- copy()¶
Return a shallow copy of a set.
- difference_update(*others)¶
Remove all elements of another set from this set.
- discard(elem)¶
Remove an element from a set if it is a member.
If the element is not a member, do nothing.
- intersection_update(*others)¶
Update a set with the intersection of itself and another.
- pop()¶
Remove and return an arbitrary set element. Raises KeyError if the set is empty.
- remove(elem)¶
Remove an element from a set; it must be a member.
If the element is not a member, raise a KeyError.
- symmetric_difference_update(other)¶
Update a set with the symmetric difference of itself and another.
- update(*others)¶
Update a set with the union of itself and others.