Ubuntu, Kickstart, preseeding, and Wireless (WLAN)

I have been trying to get an automated network install running. The starting point is a Kickstart (Kickseed) setup that works fine on LAN. Problem is: Computers do not do PXE netbooting from WLAN, and secondly that the debian installer was not configuring the network correctly. But the steps are very simple.

  1. Get mini.iso, basically the same kernel and initrd that the netbooter is running
  2. Put it on a usb drive to boot the computer (laptop) from: sudo dd if=/path/to/mini.iso of=/dev/[usb device] bs=4096
  3. Make sure that your wireless access point is not protected, just make an open one, we call it UBUNTU-INSTALL
  4. Test that you can connect to it and that the repository is working
  5. Now, boot from the USB drive and press TAB to add boot options: ks= ksdevice=wlan0 netcfg/wireless_essid=UBUNTU-INSTALL
  6. In order to add your changes to mini.iso, you need to unpack the whole filesystem and edit txt.cfg which is a file that syslinux is using to display the menu.

Changing mini.iso

Tools like UCK do not work for the mini.iso file because it’s not using stuff like Casper. So you need to create a custom boot image the hard way:

$ mkdir iso
$ sudo mount -o loop -t iso9660 mini.iso iso
$ cp -R iso custom
$ sudo chmod 600 custom/txt.cfg
$ sudo nano custom/txt.cfg
$ sudo mkisofs -r -V "Custom Ubuntu Netboot image" -cache-inodes -J -l -b isolinux.bin -c boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -o custom.iso custom-iso
$ sudo umount /media/USB-DRIVE # Remember to unmount before writing the usb
$ sudo dd if=custom.iso of=/dev/[YOU USB DRIVE] bs=4096

Limiting choices in a ModelAdmin list_filter

So, you need to limit choices, because the list of related choices is too long? Then do NOT use limit_choices_to parameter on the ForeignKey! Why? Because you risk having options missing in your forms, deleting relations unknowingly.

For instance, consider that you have the following case, expressed in pseudo code:

ModelClass.related_instance = models.ForeignKey(MyRelatedModel, limit_choices_to = {'active': True}, blank=True, null=True)
object = <ModelClass instance>
object.related_instance = <MyRelatedModel instance>
object.related_instance.active = False

If you edit object, you will see an admin form where <MyRelatedModel instance> is not selectable! Hence, you will risk editing and loosing data without knowing it.

So your best bet is to stop using limit_choices_to and instead define a filter. Luckily, it’s quite easy. It’s not beautiful, though, since admin.filters.RelatedFieldListFilter does not offer a method to overwrite.

Here is an example:

class RelatedFilter(admin.filters.RelatedFieldListFilter):
    def __init__(self, *args, **kwargs):
        super(RelatedFilter, self).__init__(*args, **kwargs)
        self.lookup_choices = [
            (x.id, x) for x in
class MyModelAdmin(admin.ModelAdmin):
    list_filter = (('related_field', RelatedFilter), 'other_related_field')

Testing and tracking down Warnings in Django

Warnings are often suppressed through many layers of control mechanisms in Python and Django. However, you should really be aware of these and clean them up once in a while!

In other cases, the Warnings reflect an inconsistent state, such as if you are mixing naive and timezone-aware variables. It can be a real pain to track down RuntimeWarning because it doesn’t leave a nice stack trace. The scenario could be that you have assigned a naive DateTime to a field, and the db layer is complaining at runtime.

However, you can run python in a mode where Warning-types raise exceptions with full stack traces. Use the following syntax:

python -W error:"":RuntimeWarning:django.db.models.fields:0 manage.py runserver
  • ‘error’ means that python should raise an exception – this is what we want!
  • ‘RuntimeWarning’ means to trigger when such is raised – you can also simply put ‘Warning’ to trigger for all Warning types.
  • ‘django.db.models.fields’ means activate for this module (you need the full path). Notice that simply putting ‘django’ does *not* activate for all of django, you need to put the specific module or simply ‘”"‘ to enable for all of Python (which might render some interesting Warning goodies that you have never seen!).
  • ’0′ means any line (you probably don’t need)

You can read more in Python Documentation chapter: Warning Control.

Django: Log and block brute force attempts

Here is a very simple mechanism for wrapping a decorator around your views to protect them against brute force attempts. For instance, if you have a secret file download available only with the right secret (/view/id/secret-hash/), you expose your view to simple brute force attempts.

Simply put, this decorator will log a 404 response object or Http404 exception, count pr. IP and return status=400 and send you an email whenever a new block is put into place.


class IllegalLookup(LogModifications):
    """Log and block illegal lookups"""
    created = models.DateTimeField(
        auto_now_add = True,
    modified = models.DateTimeField(
        auto_now = True,
    ip_address = models.CharField(
        verbose_name=_(u'IP address'),
    path = models.CharField(
        help_text=_(u'First attempted path is always logged'),
    count = models.PositiveIntegerField(
    def log_lookup(cls, ip_address, path):
            now = timezone.now()
            expired = now - timedelta(minutes=settings.BLOCK_EXPIRY)
            lookup = cls.objects.get(ip_address=ip_address,
            lookup.count += 1
        except cls.DoesNotExist:
            # Delete old entries first
            lookup = cls.objects.create(ip_address=ip_address,
    def is_blocked(cls, ip_address):
            now = timezone.now()
            expired = now - timedelta(minutes=settings.BLOCK_EXPIRY)
            lookup = cls.objects.get(ip_address=ip_address,
            if lookup.count == settings.BLOCK_ATTEMPTS:
                mail_admins("IP blocked", "{0} is now blocked, IllegalLookup id: {1}".format(ip_address, lookup.id))
            if lookup.count > settings.BLOCK_ATTEMPTS:
                return True
        except cls.DoesNotExist:
        return False


def log_and_block(func):
    def _log_and_block(request, *args, **kwargs):
        remote_ip = request.META.get('REMOTE_ADDR', None)
        if IllegalLookup.is_blocked(remote_ip):
            return HttpResponse('%s is blocked' % remote_ip,
        is_404 = False
        is_exception = False
            return_object = func(request, *args, **kwargs)
            if return_object.status_code == 404:
                is_404 = True
        except Http404:
            is_404 = True
            is_exception = True
        if is_404:
            if remote_ip:
                    request.META.get('PATH_INFO', ''))
            if is_exception:
        return return_object
    return _log_and_block


Now, simply wrap the decorator around your view:

def my_view(request, id, secret_hash):
    object = get_object_or_404(models.MyModel, id=id, secret_hash=secret_hash)

Share an internet connection: A nice little script for quickly getting the task done.

This script uses iptable forwarding and dnsmasq to share an internet connection with full relay of remote DNS servers and a local DHCP server. Before trying the script, here is the over all steps:

  • You connect to the internet in your normal fashion. For instance with a 3G dongle and your network manager applet.
  • Make sure that nothing is running on port 53, run netstat -tlnp to debug
  • READ THE SCRIPT before starting and know which interfaces you are using. EXTERNAL is the one connected to the internet and INTERNAL is the one you are sharing the connection via.
  • Do not let network-manager mange the INTERNAL interface!
  • You might have to adjust the script, especially the iwconfig part, as different interfaces may have different ways of configuring the WEP key
if [ ! `whoami` = "root" ]
  echo "Only root"
if [[ $DHCP -eq 'yes' ]]
# DHCP SERVER (enable below lines)
	apt-get install dnsmasq
        echo "listen-address=" >> $DNSMASQ_CONFIG_FILE
        echo interface=$INTERNAL >> $DNSMASQ_CONFIG_FILE
	/etc/init.d/dnsmasq stop
echo 1 > /proc/sys/net/ipv4/ip_forward
/sbin/iptables -t nat -A POSTROUTING -o $EXTERNAL -j MASQUERADE
/sbin/iptables -A FORWARD -i $EXTERNAL -o $INTERNAL -m state --state RELATED,ESTABLISHED -j ACCEPT
/sbin/iptables -A FORWARD -i $INTERNAL -o $EXTERNAL -j ACCEPT
ifconfig $INTERNAL down
iwconfig wlan0 mode ad-hoc
iwconfig wlan0 essid $SSID
iwconfig wlan0 key $WEPKEY
ifconfig $INTERNAL up
if [[ $DHCP -eq 'yes' ]]
	/etc/init.d/dnsmasq start

101% objective… always