momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Corelarea a două site-uri cu Django şi mod_wsgi

CGI | Django | Python | WSGI | virtual host
2012 sep

Constituind //xxx.sitsco.com alături de //docere.ro - în fond, două virtualHost-uri, pe acelaşi server Apache - am avut în final o mare surpriză: la început, accesarea din browser şi a unuia şi a celuilalt a decurs normal, dar brusc unul dintre ele mi-a returnat Internal server error; repetând disperat CTRL+F5 am putut reaccesa, dar situaţia s-a reprodus când am revenit pe celălalt site.

Problema nu este una accidentală, ci ţine de Django şi mod_wsgi. Mi-a luat o zi întreagă ca să mă documentez; multă lume a păţit ceva similar, dar se pare că niciuna dintre îndrumările oferite nu rezolvă complet situaţia prezentată. Înţelegând totuşi cam despre ce este vorba, am reuşit în final să rezolv chestiunea într-un minut, pe baza unor observaţii directe foarte modeste.

Pentru a reproduce situaţia menţionată, să constituim întâi două proiecte Django:

vb@vb:~$ django-admin.py startproject site_1
vb@vb:~$ django-admin.py startproject site_2

vb@vb:~$ tree site_1  ## structura unui proiect Django 1.4 ##
site_1  # containerul proiectului denumirea se poate schimba (neafectând proiectul)
├── manage.py  # program utilitar python manage.py help
└── site_1     # "proiectul" propriu-zis
    ├── __init__.py  # trebuie să existe în orice modul Python
    ├── settings.py  # setări globale sau specifice aplicaţiilor ce vor fi încorporate
    ├── urls.py      # pentru specificarea URL-urilor de acces la aplicaţiile componente 
    └── wsgi.py      # interfaţa între server (Apache) şi aplicaţiile Python

Cu manage.py putem crea un "server de lucru" (cu ecou în terminalul din care l-am lansat) şi îl putem accesa imediat din browser:

vb@vb:~$ cd site_1
vb@vb:~/site_1$ python manage.py runserver
Validating models...

0 errors found
Django version 1.4.1, using settings 'site_1.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[17/Sep/2012 10:55:32] "GET / HTTP/1.1" 200 1957 # ecoul accesării din browser

Fişierul settings.py trebuie să poată fi consultat în momentul creării serverului - vezi mai sus sublinierea "using settings" - în vederea "montării" aplicaţiilor indicate acolo ca fiind instalate. Mai mult, această linie extrasă din manage.py:

    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "site_1.settings")

asigură că settings.py va putea fi referit şi ulterior (pe parcursul utilizării serverului creat), prin intermediul variabilei "globale" DJANGO_SETTINGS_MODULE (desigur, pentru a putea fi consultat - trebuie setată în prealabil, permisiunea citirii de către alţii); de subliniat că această variabilă nu este girată prin intermediul fişierului settings.py, ea servind tocmai pentru identificarea acestuia.

Odată create (în modul standard) aceste două proiecte, să vedem cum stau lucrurile şi în cazul când am vrea să le accesăm prin HTTP. Înfiinţăm câte un virtualHost, începând prin a scrie definiţiile de bază ale acestora, conform indicaţiilor din manuale:

# fişierul  site_1_2.vh  (defineşte două VirtualHost-uri)
<VirtualHost *:80>
    ServerName site_1.loc

    WSGIScriptAlias / /home/vb/site_1/site_1/wsgi.py
    # (la accesarea site-ului, se va executa scriptul wsgi.py)

    <Directory /home/vb/site_1/site_1>
        <Files wsgi.py>
            Order deny,allow
            Allow from all  # permite accesul la fişierul wsgi.py
        </Files>
    </Directory>
</VirtualHost>

<VirtualHost *:80>
    ServerName site_2.loc
    # ... # copiem de mai sus, schimbând "site_1" cu "site_2"
</VirtualHost>

Copiem acest fişier în /etc/apache2/sites-available/ şi apelăm a2ensite (asociind un "soft-link" din /etc/apache2/sites-enabled); apoi - fiind vorba de virtualHost-uri "locale" - adăugăm o linie 127.0.0.1 site_1.loc site_2.loc în fişierul /etc/hosts şi restartăm Apache:

vb@vb:~$ sudo cp site_1_2.vh /etc/apache2/sites-available/
[sudo] password for vb: 

vb@vb:~$ sudo a2ensite site_1_2.vh 
Enabling site site_1_2.vh.
To activate the new configuration, you need to run:
  service apache2 reload

vb@vb:~$ echo  127.0.0.1 site_1.loc site_2.loc  |  sudo  tee -a  /etc/hosts
127.0.0.1 site_1.loc site_2.loc

vb@vb:~$ sudo service apache2 restart 
 * Restarting web server apache2                                                
 ... waiting .                                                           [ OK ]
vb@vb:~$ 

Acum, ar trebui să putem accesa din browser oricare dintre cele două site-uri, dar constatăm că deocamdată nu este aşa.

Consultând /var/log/apache2/error.log vedem că este vorba de: ImportError: Could not import settings 'site_1.settings' (Is it on sys.path?): No module named site_1.settings.

Înscriind în bara de căutare Google: django mod_wsgi ImportError: Could not import settings - găsim câteva mii de referinţe asupra acestei erori; am înscris desigur şi mod_wsgi, pentru că acum nu este vorba de "serverul de lucru" oferit direct de django (despre care am constatat deja mai sus că "merge"), ci de legătura cu serverul Apache.

În baza unora dintre aceste referinţe, am completat wsgi.py astfel:

import os, sys

base = os.path.dirname(os.path.abspath(__file__)) # /home/vb/site_1/site_1
base_parent = os.path.dirname(base)               # /home/vb/site_1

# we check for path because we're told to at the tail end of
# http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIReloadMechanism 

if base not in sys.path:
    sys.path.insert(0, base)  # mai sigur ca sys.path.append(base), indicat frecvent
if base_parent not in sys.path:
    sys.path.insert(0, base_parent)

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "site_1.settings")

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

Desigur, am inclus şi în site_2/wsgi.py grupul de linii referitor la "base"; restartând apoi Apache - am constatat că acum putem accesa din browser cele două site-uri, dar… cu defectul pe care l-am evocat la început: la un moment dat, accesul pe unul dintre ele conduce la Internal Server Error. Secvenţa de erori înregistrată în /var/log/apache2/error.log pentru accesul eşuat //site_1.loc este însă foarte interesantă:

[... 15:15:43 2012] [error] [client 127.0.0.1] mod_wsgi (pid=5129): 
    Exception occurred processing WSGI script '/home/vb/site_1/site_1/wsgi.py'.
...
[... ]15:15:43 2012] [error] [client 127.0.0.1] ImportError: 
    Could not import settings 'site_2.settings'...

S-a încercat procesarea scriptului site_1/wsgi.py (corespunzător cererii //site_1.loc), dar de fapt lucrurile eşuează cumva din cauza celuilalt site (vizat în mesajul de eroare 'site_2.settings')

Între procesele şi subprocesele lansate de Apache şi apoi de mod_wsgi (sau de mod_perl, etc.) există interacţiuni bine stabilite şi evocarea acestora ar clarifica lucrurile; dar măcar de data aceasta - putem evita complicaţiile unui asemenea angajament stufos.

Scripul wsgi.py redat mai sus putea să eşueze (cu mesajele de eroare indicate) numai la ultima lui linie (application = get_wsgi_application()); toate celelalte linii conţin doar instrucţiuni Python uzuale şi ca urmare sunt executate fără probleme. În particular, putem fi siguri că s-a executat şi linia:

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "site_1.settings")

iar aceasta "spune totul" şi este cheia soluţiei: în DJANGO_SETTINGS_MODULE se înscrie valoarea "site_1.settings" dacă în momentul respectiv DJANGO_SETTINGS_MODULE nu conţinea deja o valoare depusă anterior (citind atent ce face metoda setdefault() asociată dicţionarelor Python).

os.environ este un dicţionar "global", prin care aplicaţiile pot comunica serverului diverse valori (am explicat ideea pentru cazul CGI, în Extinderea functionalităţii serverului prin CGI); putem vedea valorile comune fie şi din linia de comandă: python -c 'import os; print os.environ'.

Could not import settings 'site_2.settings' din finalul mesajelor de eroare, ne spune acum că de fapt DJANGO_SETTINGS_MODULE - şi tocmai pentru motivul că am angajat "elegant" setdefault() - n-a putut fi înscrisă cu valoarea dorită 'site_1.settings', fiindcă păstra valoarea depusă anterior 'site_2.settings' (urmare a accesării la un moment imediat anterior, a site-ului //site_2.loc).

Rezultă că trebuie să forţăm înscrierea valorii necesare, renunţând la setdefault() (ignorând astfel, unele maniere "pythoniste"):

os.environ["DJANGO_SETTINGS_MODULE"] = "site_1.settings"

Făcând această corectură pe ambele fişiere wsgi.py şi restartând Apache putem constata că acum ambele site-uri pot fi accesate unul după altul, fără să mai apară "Internal Server Error". WSGI, Python, etc. evoluează continuu de la o versiune la alta - dar deocamdată nu vedem nici un motiv (excluzând eleganţa "pythonistă") pentru care această soluţie simplă să-şi piardă valabilitatea.

vezi Cărţile mele (de programare)

docerpro | Prev | Next