Commit c258deb1 authored by Baptiste Jonglez's avatar Baptiste Jonglez
Browse files

Merge remote-tracking branch 'origin/simple_dsl'

parents d183023e 0cc7f6d2
How to extend Coin
==================
Coin can be extended by writing your own backends, so that you can support
various access technologies by using the tools you want.
The common part of Coin already handles the administrative details of
subscriptions: members, subscriptions cost and billing strategy,
generating bills, etc.
It also handles the IP allocation process. The administrator can define
pools of IP addresses, in which subnets are automatically allocated to
subscribers. Each subscription can be attached any number of IPv4 or IPv6
subnets, which somehow get routed to the subscriber (the actual way of doing
this depends on the backend). Management of reverse DNS is an upcoming
feature.
All the rest is up to you, that is, the technical side of subscriptions.
You will probably want to handle:
- authentication (e.g. Radius login and password, TLS certificates...)
- accounting (e.g. collecting and displaying graphs of user traffic)
- routing (e.g. inserting static routes, or signalling a routing daemon
about new routes)
- technology-specific information (e.g. phone number associated to a DSL
line, MAC address of a CPE, GPS coordinates for wireless subscribers)
- stuff we didn't think about when writing this
This can be done in three steps:
- write a Django application, whose models describe the data you need
- optionally, implement views for presenting some of this information to users
- write a backend to distribute needed configuration data to the rest of
the infrastructure (routers, switches, log servers, accounting backend...)
How you implement the actual backend is completely up to you. It can be a
SQL database (useful for Radius), a LDAP database, simply inserting static
routes in the kernel (if Coin runs on one of your routers, which is
probably not a good idea), writing configuration to text files, relying on
an orchestration tool such as Puppet or Ansible, etc.
Simple example: no view, no backend
-----------------------------------
A very simple application is provided with Coin: it's called `simple_dsl`.
This application provides a simple model for DSL subscribers (just a phone
number, no authentication), and doesn't use any backend. It is intended
more as a demonstration, but it is perfectly usable, and should fulfil the
needs of small ISPs selling "white label" DSL lines.
It is probably a good starting point for writing your own application. If
you need more features, read on.
More complex example with views and a LDAP backend
--------------------------------------------------
See `coin/vpn` for a much more complex application: OpenVPN access with
login/password and an arbitrary number of subnets routed to the user. The
user has an interface for generating a password and for choosing which IP
addresses it wants to use. All this configuration data is pushed to a
LDAP database, which is then used by the OpenVPN server. OpenVPN
interfaces with LDAP both natively (for authenticating users) and through
shell scripts (for routes and IP addresses).
Complete specification
======================
Models
------
Your model must inherit from `coin.configuration.models.Configuration`.
This way, it will be automatically integrated in the generic admin
interface, and will gain the ability to be associated to IP subnets.
If you define a Meta class with a `verbose_name` attribute, it will be
used to identify your configuration backend in the interface (otherwise
the name of the class will be used).
If you want to provide views for your model, you must define an
`url_namespace` attribute, which is a string defining the URL namespace
associated to your view. By default, the (lowercased) name of the class
will be used.
You should also define a `subnet_event(self)` method, which will be called
whenever the IP subnets associated to a configuration object have changed
(new subnet, deleted subnet, modified subnet). You can use the
`ip_subnet` related name to have access to all IP subnets associated to
the object (for instance, `self.ip_subnet.all()` will give you a list of
`coin.resources.models.IPSubnet` objects).
Note that, like all Django models, you should define a `__unicode__`
method to describe an object of your class.
Admin
-----
Your admin model must inherit from
`coin.configuration.admin.ConfigurationAdminFormMixin` and
`polymorphic.admin.PolymorphicChildModelAdmin` (in this order).
Otherwise, it's a perfectly regular admin model (see `simple_dsl`), except
for the specificities described below.
You must define a `inline` attribute, set to an inline admin model for
your model (for instance, built on `admin.StackedInline` or
`admin.TabularInline`; again, see `simple_dsl`). This inline model will
be used in the generic admin interface, so that administrators can edit
the backend details directly from a subscription object.
If you don't have any view, remember to set the `view_on_site` attribute
to `False`, so that Django's admin will not show a "View on site" button.
Views
-----
If you want to provide views for your model, you must provide at least a
"details" view, which will be used to display information about your
configuration objects to end-users. For instance, you can inherit from
`django.views.generic.detail.DetailView`, or
`django.views.generic.edit.UpdateView` if you want users to edit some of
the fields (see `coin/vpn/views.py`).
Here is an example URL pattern to be used in your `urls.py`:
url(r'^(?P<id>\d+)$', VPNView.as_view(template_name="vpn/vpn.html"), name="details")
Note that this pattern **must** be called "details". The global `urls.py`
should contain a pattern of the form:
url(r'^vpn/', include('coin.vpn.urls', namespace='vpn'))
where the value of "namespace" is the URL namespace defined in your
original model (see above).
Of course, you can add as many additional views as you want.
......@@ -4,12 +4,11 @@ The COIN project
`Coin` is Illyse's Information System, designed to manage both members
and Internet accesses, such as through DSL, VPN, wireless…
It is written in Django, and makes an heavy use of LDAP (for
authentication, and to store configuration information).
It features a generic configuration interface, which allows to implement
custom backends for different technologies. Currently implemented is a
LDAP-based backend for OpenVPN.
It is written in Django, and features a generic configuration interface,
which allows to implement custom backends for different technologies.
Currently implemented is a LDAP-based backend for OpenVPN, and a very
simple DSL backend, without any authentication (useful for "white label"
DSL).
Coin currently only works with python2, because `python-ldap` is (as of
2013) not compatible with python3.
......@@ -18,7 +17,11 @@ The project page (issue, wiki, etc) is here:
https://www.illyse.org/projects/ils-si/
A mirror of the code is available at:
The code is available at:
git://git.illyse.org:coin
A mirror of the code, with a web interface, is also available at:
https://code.ffdn.org/zorun/coin/
......@@ -36,6 +39,12 @@ Password: internet
This user account has access to the administration interface.
Extending Coin
==============
If you want to write your own backend, see `EXTENDING.md`.
Quickstart
==========
......@@ -145,6 +154,13 @@ Configuration
You should first setup the `sites` application, in the admin. The domain name
configured there is used for outgoing emails.
LDAP
----
By default, LDAP support is disabled. If you want to use LDAP (for
instance for the OpenVPN/LDAP backend, or for your own backend), see the
configuration file `coin/settings_local.example-illyse.py`.
ISP-specific configuration
--------------------------
......
......@@ -55,5 +55,8 @@ class ParentConfigurationAdmin(PolymorphicParentModelAdmin):
class ConfigurationAdminFormMixin(object):
base_form = ConfigurationForm
# For each child (admin object for configurations), this will display
# an inline form to assign IP addresses.
inlines = (IPSubnetInline, )
admin.site.register(Configuration, ParentConfigurationAdmin)
......@@ -61,7 +61,7 @@ class Configuration(PolymorphicModel):
celui définit dans la classe enfant dans url_namespace sinon
par défaut utilise le nom de la classe en minuscule
"""
if self.url_namespace:
if hasattr(self, 'url_namespace') and self.url_namespace:
return self.url_namespace
else:
return self.model_name().lower()
......@@ -71,38 +71,39 @@ class Configuration(PolymorphicModel):
@receiver(post_save, sender=IPSubnet)
def subnet_save_event(sender, **kwargs):
"""Fires when a subnet is saved (created/modified). We tell the
@receiver(post_delete, sender=IPSubnet)
def subnet_event(sender, **kwargs):
"""Fires when a subnet is created, modified or deleted. We tell the
configuration backend to do whatever it needs to do with it.
We should use a pre_save signal, so that if anything goes wrong in the
backend (exception raised), nothing is actually saved in the database.
But it has a big problem: the configuration backend will not see the
change, since it has not been saved into the database yet.
That's why we use a post_save signal instead. But surprisingly, all
is well: if we raise an exception here, the IPSubnet object will not
be saved in the database. But the backend *does* see the new state of
the database. It looks like the database rollbacks if an exception is
raised. Whatever the reason, this is not a documented feature of
Django signals.
"""
subnet = kwargs['instance']
try:
config = subnet.configuration
config.save_subnet(subnet, kwargs['created'])
except ObjectDoesNotExist:
pass
Note that we could provide a more advanced API to configurations
(subnet created, subnet modified, subnet deleted), but this is quite
complicated to do. It's much simpler to simply tell the configuration
model that something has changed in IP subnets. The configuration
model can then access the list of its associated subnets (via the
"ip_subnet" attribute) to decide for itself what it wants to do.
We should use a pre_save/pre_delete signal, so that if anything goes
wrong in the backend (exception raised), nothing is actually saved in
the database: this provides consistency between the database and the
backend. But if we do this, there is a major issue: the configuration
backend will not see the new state of subnets by querying the
database, since the changes have not been saved into the database yet.
That's why we use a post_save/post_delete signal instead. In theory,
this is a bad idea, because if the backend fails to do whatever it
needs to do, the subnet will be saved into Django's database anyway,
causing a desynchronisation with the backend. But surprisingly, even
if not a documented feature of Django's signals, all is well: if we
raise an exception here, the IPSubnet object will not be saved in the
database. It looks like the database rollbacks if an exception is
raised, which is great (even if undocumented).
@receiver(post_delete, sender=IPSubnet)
def subnet_delete_event(sender, **kwargs):
"""Fires when a subnet is deleted. We tell the configuration backend to
do whatever it needs to do with it.
"""
subnet = kwargs['instance']
try:
config = subnet.configuration
config.delete_subnet(subnet)
if hasattr(config, 'subnet_event'):
config.subnet_event()
except ObjectDoesNotExist:
pass
......@@ -22,7 +22,7 @@
<td>{{ subscription.subscription_date }}</td>
<td>{{ subscription.resign_date|default_if_none:"" }}</td>
<td>{{ subscription.configuration.comment }}</td>
<td>{% if subscription.configuration %}<a class="cfglink" href="{% url subscription.configuration.get_url_namespace|add:":details" id=subscription.configuration.id %}"><i class="fa fa-cog"></i> Configuration</a>{% endif %}</td>
<td>{% if subscription.configuration and subscription.configuration.url_namespace %}<a class="cfglink" href="{% url subscription.configuration.get_url_namespace|add:":details" id=subscription.configuration.id %}"><i class="fa fa-cog"></i> Configuration</a>{% endif %}</td>
</tr>
{% endfor %}
......
......@@ -161,7 +161,8 @@ INSTALLED_APPS = (
'coin.reverse_dns',
'coin.configuration',
'coin.vpn',
'coin.isp_database'
'coin.isp_database',
'simple_dsl'
)
# A sample logging configuration. The only tangible logging
......
......@@ -5,7 +5,7 @@ from django.contrib import admin
from polymorphic.admin import PolymorphicChildModelAdmin
from coin.vpn.models import VPNConfiguration
from coin.configuration.admin import ConfigurationAdminFormMixin, IPSubnetInline
from coin.configuration.admin import ConfigurationAdminFormMixin
from coin.utils import delete_selected
......@@ -30,8 +30,6 @@ class VPNConfigurationAdmin(ConfigurationAdminFormMixin, PolymorphicChildModelAd
"generate_endpoints_v6", "activate", "deactivate")
exclude = ("password",)
inline = VPNConfigurationInline
# TODO: this should be done in the generic configuration admin.
inlines = (IPSubnetInline, )
def get_readonly_fields(self, request, obj=None):
if obj:
......
......@@ -44,16 +44,14 @@ class VPNConfiguration(CoinLdapSyncMixin, Configuration):
def get_absolute_url(self):
return reverse('vpn:details', args=[str(self.pk)])
# These two methods are part of the general configuration interface.
def save_subnet(self, subnet, creation):
# This method is part of the general configuration interface.
def subnet_event(self):
self.check_endpoints(delete=True)
# We potentially changed the endpoints, so we need to save.
# We potentially changed the endpoints, so we need to save. Also,
# saving will update the subnets in the LDAP backend.
self.full_clean()
self.save()
def delete_subnet(self, subnet):
self.save_subnet(subnet, False)
def get_subnets(self, version):
subnets = self.ip_subnet.all()
return [subnet for subnet in subnets if subnet.inet.version == version]
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from polymorphic.admin import PolymorphicChildModelAdmin
from coin.configuration.admin import ConfigurationAdminFormMixin
from simple_dsl.models import SimpleDSL
class SimpleDSLInline(admin.StackedInline):
model = SimpleDSL
class SimpleDSLAdmin(ConfigurationAdminFormMixin, PolymorphicChildModelAdmin):
base_model = SimpleDSL
# Used for form inclusion (when browsing a subscription object in the
# admin, SimpleDSLInline will be displayed)
inline = SimpleDSLInline
# Since we don't provide a view, don't display a "view on site" link
# in the admin.
view_on_site = False
admin.site.register(SimpleDSL, SimpleDSLAdmin)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('configuration', '0003_configuration_comment'),
]
operations = [
migrations.CreateModel(
name='SimpleDSL',
fields=[
('configuration_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='configuration.Configuration')),
('phone_number', models.CharField(help_text='Phone number associated to the DSL line', max_length=20, verbose_name='phone number')),
],
options={
'verbose_name': 'DSL line',
},
bases=('configuration.configuration',),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models
from coin.configuration.models import Configuration
class SimpleDSL(Configuration):
"""Very simple DSL model, mostly to demonstrate the use of the generic
functionality of COIN. There is no real configuration backend, and no
authentication data. But this still allows to track the phone number
and IP addresses of subscribers, which may be useful for "white label"
DSL reselling.
"""
class Meta:
verbose_name = 'DSL line'
# If Django's default pluralisation is not satisfactory
#verbose_name_plural = 'very many DSL lines'
# URL namespace associated to this configuration type, to build URLs
# in various view. Should also be defined in urls.py. Here, we don't
# define any view, so there's no need for an URL namespace.
#url_namespace = "dsl"
phone_number = models.CharField(max_length=20,
verbose_name='phone number',
help_text="Phone number associated to the DSL line")
def __unicode__(self):
return self.phone_number
def subnet_event(self):
# Do something with self.ip_subnet.all() here.
pass
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.test import TestCase
# Create your tests here.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.shortcuts import render
# Create your views here.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment