InEnglish

[TIPS] Django : How to select only base classes in a query (remove subclasses)

Monday, 04 July 2016
|
Écrit par
Grégory Soutadé

This was my problem for Dynastie (a static blog generator). I have a main super class Post and a derived class Draft that directly inherit from the first one.

class Post(models.Model):
    title = models.CharField(max_length=255)
    category = models.ForeignKey(Category, blank=True, null=True, on_delete=models.SET_NULL)
    creation_date = models.DateTimeField()
    modification_date = models.DateTimeField()
    author = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    description = models.TextField(max_length=255, blank=True)
    ...

class Draft(Post):
    pass

A draft is a note that will not be published soon. When it's published, the creation date is reset. Using POO and inheritance, it's quick and easy to model this behavior. Nevertheless, there is one problem. When I do Post.objects.all() I get all Post objects + all Draft objects, which is not what I want !!

The trick to obtain only Post is a mix with Python and Django mechanisms called Managers. Managers are at the top level of QuerySet construction. To solve our problem, we'll override the models.Model attribute objects (which is a models.Manager).

Inside inheritance

To find a solution, we need to know what exactly happens when we do inheritance. The best thing to do, is to inspect the database.

CREATE TABLE "dynastie_post" (
    "id" integer NOT NULL PRIMARY KEY,
    "title" varchar(255) NOT NULL,
    "category_id" integer REFERENCES "dynastie_category" ("id"),
    "creation_date" datetime NOT NULL,
    "modification_date" datetime NOT NULL,
    "author_id" integer REFERENCES "auth_user" ("id"),
    "description" text NOT NULL);

CREATE TABLE "dynastie_draft" (
    "post_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "dynastie_post" ("id")
);

We can see that dynastie_draft has a reference to the dynastie_post table. So, doing Post.objects.all() is like writing "SELECT * from dynastie_post" that includes Post part of drafts.

Solution 1 : Without altering base class

The first solution is to create a Manager that will exclude draft id from the request. It has the advantage to keep base class as is, but it's not efficient (especially if there is a lot of child objects).

class PostOnlyManager(models.Manager):
    def get_queryset(self):
        query_set = super(PostOnlyManager, self).get_queryset()
        drafts = Draft.objects.all().only("id")
        return query_set.exclude(id__in=[draft.id for draft in drafts])

class Post(models.Model):
    objects = PostOnlyManager()

class Draft(Post):
    objects = models.Manager()

With this solution, we do two requests at each access. Plus, it's necessary to know every sub class we want to exclude. We have to keep the BaseManager for all subclasses. You can note the use of only method to limit the query and de serialization to minimum required.

Solution 2 : With altering base class

The solution here is to add a field called type that will be filtered in the query set. It's the recommended one in the Django documentation.

class PostOnlyManager(models.Manager):
    def get_query_set(self):
        return super(PostOnlyManager, self).get_queryset().filter(post_type='P')

class Post(models.Model):
    objects = PostOnlyManager()
    post_type = models.CharField(max_length=1, default='P')

class Draft(Post):
    objects = models.Manager()

@receiver(pre_save, sender=Draft)
def pre_save_draft_signal(sender, **kwargs):
    kwargs['instance'].post_type = 'D'

The problem here is the add of one field which increase database size, but filtering is easier and is done in the initial query. Plus, it's more flexible with many classes. I used a signal to setup post_type value, didn't found a better solution for now.

Conclusion

Depending on your constraints you can use the solution 1 or 2. These solutions can also be extended to a more complex filtering mechanism by dividing your class tree in different families.

How to modify SQLite3 schemas

Sunday, 31 January 2016
|
Écrit par
Grégory Soutadé

SQLite3 is a wonderful library allowing to have a tiny SQL database in a simple file rather than a big (but more powerful) SQL server. I use it a lot for my projects to be "self contained".

Unfortunately, SQLite3 has some limitations. One of them is the unavailability to modify columns constraints or to delete them.

By upgrading from Django 1.5 to Django 1.8, I found that I cannot add a new user to my database. The problem is that the constraint of the last_login column in the User schema (controlled by Django framework) has changed. Now it can be NULL, while the table was created with a non NULL constraint. I got this exception :

NOT NULL constraint failed: auth_user.last_login

I already faces this kind of situation. The solution is pretty simple and does not requires modifying code.

First of all, make a backup of your database.

To get round this problem, we will export database in SQL commands format, modify it with a simple text editor and import it again.

Assume that your database is named denote.db

$ cp denote.db denote.db.bak
$ sqlite3 denote.db
$ .output denote.sql
$ .dump
$ .exit
$ rm denote.db
$ emacs denote.sql
...
$ sqlite3 -init denote.sql denote.db
$ sudo apachectl restart

That's all !

For complex modifications, it can be long/boring, so I recommend to use tools like sed, perl or python scripts, regexp replace mode from emacs...

LUKS on Cubox (imx6 platform)

Sunday, 09 August 2015
|
Écrit par
Grégory Soutadé

Now I have a Cubox (i2ex), I wanted to encrypt my backup disks. I followed this excellent tutorial. The cipher recommendation is aes-xts-plain64. So, I created formated disk, encrypt it, copy data from my computer and connect it to the Cubox.

But, I get this error :

cryptsetup --type luks open /dev/sda1 backup
Enter passphrase for /dev/sda1: 
No key available with this passphrase.

I tried a couple of times, use a keyfile. Nothing worked !! It made me crazy. After a bunch of search, I found that the current kernel in the Debian image from Igor Pečovnik is 3.14.14 and has issues with NEON AES implementation.

I cannot blacklist the module as it's builtin. The solution is to build a new module from the latest 3.14.49 kernel (maintained by Greg Kroah-Hartman). Ouf ! I increased cra_priority to be sure to use the new implementation.

Instructions

In the Cubox :

  • Install kernel headers (already here in the image)
  • Download the latest 3.14 kernel from kernel.org
  • Decompress it
  • Apply this patch
  • Compile
  • Install
  • Reboot

Commands :

patch -p1 < crypto.patch
cd linux-3.14.49/arch/arm/crypto/
make
sudo make install
sudo echo aes-arm-bs >> /etc/modules
reboot

If you just want to test :

insmod aes-arm-bs.ko

Precompiled version

A precompiled version is available here. To manually install it :

sudo cp aes-arm-bs.ko /lib/modules/3.14.14-cubox-i/extra/
sudo depmod
sudo echo aes-arm-bs >> /etc/modules

Or just

sudo make install
sudo echo aes-arm-bs >> /etc/modules

How to make custom ROM for Cybook e-readers

Monday, 06 July 2015
|
Écrit par
Grégory Soutadé

Bookeen doesn't provides any way to hack their e-ink e-readers. Nevertheless, after they introduce a secured update file format for the first Cybook serie, they come back to a "tar-like" format for their e-readers based on Allwiner platform (new Odyssey, Muse, Nolimbook...). With this format, they try to be closest to Android platform.

Be careful : handling all bookeen's readers is difficult because there is a lot of derived/customized readers available ! For this, Bookeen has its own server that checks for serial number to determine which update to serve. A special attention must be done with manipulating boot and bootloader images, it can bricks your e-reader !

This tutorial will only works with UNIX/Linux tools, I do not plan to do it for Windows.

So, let's start. An archive is commonly named CybUpdate.bin. In facts it's a .tar.bz2 file.

First, decompresse it :

mkdir decompressed
cd decompressed
tar -jxvf ../CybUpdate.bin

The following content should be created :

contents
bootloader.fex
boot.fex
rootfs.fex

I think that boot.fex and bootloader.fex are optional, but not sure. Two types of files are present :

  • contents that contains meta information
  • fex files

Contents has the following format :

<ident>|<filename>|<length>|<sha256sum>|<version>

idents are :

  • LOAD for bootloader
  • BOOT for boot partition
  • ROOT for rootfs

Fex extension is a generic one that actually is flash images in different format (vfat, ext...).

In bootloader.fex and rootfs.fex we have a file "/version" specifying the current version (allowing to do checks). Mounting bootloader and rootfs is quite easy :

mkdir root
sudo mount -t ext2 rootfs.fex root -o loop

mkdir bootloader
sudo mount -t vfat bootloader.fex bootloader -o loop

After doing modifications, just unmount the directory and the image is automatically generated ! (Don't forget to update contents metadata).

boot.fex is more complex, it has Android bootloader format. You have to use split_bootimg.pl to decompress it.

mkdir boot
cd boot
../split_bootimg.pl ../boot.fex
> Page size: 2048 (0x00000800)
> Kernel size: 10863524 (0x00a5c3a4)
> Ramdisk size: 2253456 (0x00226290)
> Second size: 0 (0x00000000)
> Board name: sun5i
> Command line: 
> Writing boot.fex-kernel ... complete.
> Writing boot.fex-ramdisk.gz ... complete.

boot.fex-kernel is a binary version (compiled) of Linux.

Then we decompress ramdisk :

mkdir ramdisk_decompressed
cd ramdisk_decompressed
gzip -dc ../boot.fex-ramdisk.gz | cpio -i

You can re compress it with :

find | cpio -o | gzip -c > ../boot.fex-ramdisk.gz

Rebuilding boot is done with mkbootimg. Be careful to use the same parameters split_bootimg.pl displayed !

./mkbootimg.py --kernel boot.fex-kernel --ramdisk boot.fex-ramdisk.gz --pagesize 2048 --board sun5i -o ../boot.fex

As you see, there is nothing complicated here, but mistakes with boot/bootloader or init scripts can bricks your e-reader.

Have fun !

Mixing Apache2 and nginx log file

Wednesday, 27 May 2015
|
Écrit par
Grégory Soutadé

My configuration is a bit specific. I have an Apache server for PHP and Python stuff with nginx as a frontend (and for serving static files like this blog). This part seems normal for most of people. BUT, in my case, I want that the two servers write the access logs IN THE SAME FILE (/var/log/apache2/access.log)

I run a Debian stable on my server. With the previous version (7.0), all was fine, but, until Jessie (8.0), I found a very strange behaviour with my web statistics analyzer. I usually have from 60 to 80 visits per day. In may (after the upgrade to Jessie), I get only 2 or 3 visits per day, using only one subdomain. Plus, it's a period where I have no internet at home and no time to investigate...

Finally, I found the problem ! It's located in logrotate. The two files are :

/etc/logrotate.d/apache2

/var/log/apache2/*.log {
    daily
    missingok
    rotate 14
...
    postrotate
            if /etc/init.d/apache2 status > /dev/null ; then \
                /etc/init.d/apache2 reload > /dev/null; \
            fi;
    endscript
...
}

And /etc/logrotate.d/nginx

/var/log/nginx/*.log {
    weekly
    missingok
    rotate 52
...
    postrotate
            invoke-rc.d nginx rotate >/dev/null 2>&1
    endscript
}

The first one do a logrotate every day of all files in /var/log/apache2/*.log (that contains our . The second one, do only one logrotate every week. But, the most important line is located in postrotate. Why it's important ? Because it says to nginx and Apache "Now the log file has changed, close the previous and open the new one". Seems correct, but without that, nginx write in /var/log/apache2/access.log.1 and not /var/log/apache2/access.log until it gets rotated (after a week) because it will keep the handle on access.log that is renamed to access.log.1 by logrotate.

The solution is to add

invoke-rc.d nginx rotate >/dev/null 2>&1

In the postrotate section of /etc/logrotate.d/apache2 (after the if). Then, restart nginx :

systemctl restart nginx