0xGA: Check-in [341670f849]

Yet another PHP framework, but made for org-mode and geeks.

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add logging facility. Ready to serve my own website
Timelines: family | ancestors | descendants | both | narv
Files: files | file ages | folders
SHA1:341670f84985992cce383378e9a46532b1751d50
User & Date: milouse 2014-05-04 16:55:06
Context
2014-05-04
17:05
Update the doc check-in: 0fd14e3ac6 user: milouse tags: narv
16:55
Add logging facility. Ready to serve my own website check-in: 341670f849 user: milouse tags: narv
2014-05-03
20:12
Fix various bugs. Seems very close of a prod ready status. check-in: e913d85986 user: milouse tags: narv
Changes

Added example/milouse.py.

            1  +#!/usr/bin/env python3
            2  +# -*- coding: utf-8 -*-
            3  +
            4  +import os
            5  +import re
            6  +from datetime import datetime
            7  +from configparser import ConfigParser
            8  +from narv import IcanDoThat
            9  +from narv import Narv
           10  +
           11  +class MilouseBlog(IcanDoThat):
           12  +
           13  +    appname = 'milouse'
           14  +
           15  +    monthes = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin',
           16  +    'juillet', 'août', 'septembre', 'octobre', 'novembre', 'decembre']
           17  +
           18  +    def archive(self, pattern=None):
           19  +        if 'blog_folder' in self.config:
           20  +            blog_folder = self.config['blog_folder']
           21  +        else:
           22  +            blog_folder = 'blog'
           23  +
           24  +        blog_index = []
           25  +        blog_folder = os.path.join('srv', self.appname, blog_folder)
           26  +
           27  +        if os.path.isdir(blog_folder):
           28  +
           29  +            for article_permalink in os.listdir(blog_folder):
           30  +                article_local_path = os.path.join(blog_folder, article_permalink)
           31  +                meta_file = os.path.join(article_local_path, 'meta.conf')
           32  +
           33  +                if os.path.isdir(article_local_path) and os.path.exists(meta_file) and os.path.isfile(meta_file):
           34  +                    meta_infos = ConfigParser()
           35  +                    meta_infos.read(meta_file)
           36  +
           37  +                    if 'metadata' in meta_infos:
           38  +                        meta_infos = meta_infos['metadata']
           39  +
           40  +                        if 'timestamp' in meta_infos and (pattern == None or meta_infos['timestamp'][0:len(pattern)] == pattern):
           41  +
           42  +                            title = ''
           43  +                            if 'title' in meta_infos:
           44  +                                title = meta_infos['title']
           45  +                                title = re.sub('"', '', title)
           46  +
           47  +                            blog_index.append((
           48  +                                meta_infos['timestamp'],
           49  +                                title,
           50  +                                article_permalink))
           51  +
           52  +        return sorted(blog_index, key=lambda art: art[0], reverse=True)
           53  +
           54  +
           55  +    def blog(self):
           56  +        self.extension = 'html'
           57  +
           58  +        date_infos = re.search(r'(\d{4})/(?:(\d{2})/)?$', self.path)
           59  +        articles = []
           60  +
           61  +        if date_infos and date_infos.groups()[0]:
           62  +            date_tuple = date_infos.groups()
           63  +            date_query = date_tuple[0]
           64  +
           65  +            if date_tuple[1]:
           66  +                date_query = '{0}{1}'.format(*date_tuple)
           67  +
           68  +            articles = self.archive(date_query)
           69  +
           70  +        else:
           71  +            articles = self.archive()
           72  +
           73  +        output = "<ul>"
           74  +        for art in articles:
           75  +            art_date = datetime.strptime(art[0], "%Y%m%d%H%M%S")
           76  +            output += '<li><a href="/a-écrit/{0}/{1}/">{2}</a> {3}</li>'.format(
           77  +                art_date.strftime("%Y/%m/%d"),
           78  +                art[2],
           79  +                art[1],
           80  +                'le {0} {1} {2} à {3}'.format(
           81  +                    art_date.day,
           82  +                    '<a href="/a-écrit/{0}/">{1}</a>'.format(
           83  +                        art_date.strftime("%Y/%m"),
           84  +                        self.monthes[int(art_date.month) - 1]
           85  +                    ),
           86  +                    '<a href="/a-écrit/{0}/">{0}</a>'.format(
           87  +                        art_date.year
           88  +                    ),
           89  +                    art_date.strftime("%H:%M")))
           90  +
           91  +        output += '</ul>'
           92  +
           93  +        if 'blog_template' in self.config:
           94  +            blog_template = os.path.join('srv', self.appname, self.config['blog_template'])
           95  +
           96  +            if os.path.isfile(blog_template):
           97  +                temp_content = ''
           98  +                with open(blog_template) as f:
           99  +                    temp_content = f.read()
          100  +
          101  +                output = re.sub(
          102  +                    r'<p>\s+Aucun article disponible&#x2026;\s+</p>',
          103  +                    output,
          104  +                    temp_content,
          105  +                    re.M)
          106  +
          107  +        return bytes(output, 'utf-8')
          108  +
          109  +
          110  +if __name__ == "__main__":
          111  +    app = Narv(('localhost', 8000), MilouseBlog)
          112  +    app.start()

Changes to narv.

    29     29   }
    30     30   
    31     31   function narv_start {
    32     32       if [ -f "$WORKINGREP/var/$APPNAME.pid" ] ; then
    33     33           echo "Your Narv application is already running."
    34     34           exit 1;
    35     35       fi
           36  +
    36     37       echo -n "Starting $WORKINGREP/usr/bin/$APPNAME.py"
    37         -    $WORKINGREP/usr/bin/$APPNAME.py &>> $WORKINGREP/var/log/$APPNAME.log &
           38  +    $WORKINGREP/usr/bin/$APPNAME.py &
    38     39       pgrep -f $APPNAME.py > $WORKINGREP/var/$APPNAME.pid
    39     40       echo "                   [OK]"
    40     41   }
    41     42   
    42     43   function narv_stop {
    43     44       if [ ! -f "$WORKINGREP/var/$APPNAME.pid" ] ; then
    44     45           echo "Your Narv application is NOT running."
................................................................................
    55     56           echo "Your Narv application is NOT running."
    56     57           exit 1;
    57     58       fi
    58     59       echo -n "Stopping $WORKINGREP/usr/bin/$APPNAME.py"
    59     60       pkill -f $APPNAME.py
    60     61       echo "                   ..."
    61     62       echo -n "Starting $WORKINGREP/usr/bin/$APPNAME.py"
    62         -    $WORKINGREP/usr/bin/$APPNAME.py &>> $WORKINGREP/var/log/$APPNAME.log &
           63  +    $WORKINGREP/usr/bin/$APPNAME.py &
    63     64       pgrep -f $APPNAME.py > $WORKINGREP/var/$APPNAME.pid
    64     65       echo "                   [OK]"
    65     66   }
    66     67   
    67     68   function init_chroot {
    68     69       if [ "$UID" != "0" ] ; then
    69     70           echo "You must be root to create the chroot"
................................................................................
   193    194           echo ":: configuring new Website"
   194    195           install -dm 755 $WORKINGREP/home/$APPNAME/.themes
   195    196           install -dm 755 $WORKINGREP/srv/$APPNAME
   196    197   
   197    198           exec 6>&1 # bind fd #6 to stdout (save stdout)
   198    199           exec > $WORKINGREP/etc/rc.conf # redirect stdout to etc/rc.conf
   199    200           cat <<EOF
          201  +[general]
          202  +debug = info
          203  +
   200    204   [$DOMAINNAME]
   201         -debug = 0
   202         -appname = $APPNAME
          205  +something = wonderfull
   203    206   EOF
   204    207   
   205    208           exec > $WORKINGREP/etc/routes.conf
   206    209           cat <<EOF
   207    210   [$DOMAINNAME]
   208    211   / = my_first_custom_path
   209    212   EOF
................................................................................
   213    216   #!/usr/bin/env python3
   214    217   # -*- coding: utf-8 -*-
   215    218   
   216    219   from narv import IcanDoThat
   217    220   from narv import Narv
   218    221   
   219    222   class MyBeautifulApp(IcanDoThat):
          223  +
          224  +    appname = '$APPNAME'
   220    225   
   221    226       def my_first_custom_path(self):
   222    227           self.extension = 'html'
   223    228           return b'<h1>Hello World!</h1>'
   224    229   
   225    230   
   226    231   if __name__ == "__main__":

Changes to narv.py.

     7      7   import pwd
     8      8   import grp
     9      9   import sys
    10     10   import signal
    11     11   import urllib
    12     12   import shutil
    13     13   import fnmatch
           14  +import logging
    14     15   import threading
    15     16   import configparser
           17  +from datetime import date
    16     18   from http.server import HTTPServer
    17     19   from http.server import BaseHTTPRequestHandler
    18     20   from socketserver import ThreadingMixIn
    19     21   from configparser import ConfigParser
    20     22   
    21     23   
    22         -NARV_DEBUG = True
    23         -def log(text, force=False):
    24         -    global NARV_DEBUG
    25         -
    26         -    if NARV_DEBUG or force:
    27         -        print(text)
    28         -
    29     24   
    30     25   class ResourceHelper:
    31     26       """ Utility to extract and expose various information on resources served """
    32     27   
    33     28       content_types = {
    34     29           'html': 'text/html; charset=utf-8',
    35     30           'jpg': 'image/jpeg',
................................................................................
    79     74               ext = ext[1:]
    80     75   
    81     76           if ext != '' and ext in self.content_types:
    82     77   
    83     78               self.resource_ext = ext
    84     79               self.content_type = self.content_types[ext]
    85     80   
    86         -            log('[{0}] has extension {1} and content_type {2}'.format(
           81  +            logging.debug('[{0}] has extension {1} and content_type {2}'.format(
    87     82                   self.resource_path, self.resource_ext, self.content_type))
    88     83   
    89     84   
    90     85       def test_local_file_exists(self, request):
    91     86   
    92         -        log("[{0}] Test if local file {0} exists.".format(request))
           87  +        logging.debug("[{0}] Test if local file {0} exists.".format(request))
    93     88   
    94     89           if os.path.isfile(request):
    95         -            log("[{0}] Match.".format(request))
           90  +            logging.debug("[{0}] Match.".format(request))
    96     91               self.resource_path = request
    97     92               return True
    98     93   
    99     94           return False
   100     95   
   101     96   
   102     97       def test_method_exists(self, request):
   103     98   
   104         -        log("[{0}] Test if method {0} exists in your App".format(request))
           99  +        logging.debug("[{0}] Test if method {0} exists in your App".format(request))
   105    100           if request in dir(self.App):
   106         -            log("[{0}] Match".format(request))
          101  +            logging.debug("[{0}] Match".format(request))
   107    102               self.resource_path = request
   108    103               self.resource_is_method = True
   109    104               return True
   110    105   
   111    106   
   112    107       def extract_interesting_routes(self):
   113    108           interesting_routes = []
   114    109           tokens = False
   115    110   
   116    111           for potential_path in self.routes:
   117    112               if fnmatch.fnmatch(self.request_path, potential_path):
   118         -                log("[{0}] Found potential match with {1}".format(
          113  +                logging.debug("[{0}] Found potential match with {1}".format(
   119    114                       self.request_path, potential_path
   120    115                   ))
   121    116   
   122    117                   expand_wildcards = re.sub(r'\*', '(.+)', potential_path)
   123    118                   tokens = re.search(
   124    119                       expand_wildcards,
   125    120                       self.request_path
................................................................................
   134    129   
   135    130       def find_route(self):
   136    131   
   137    132           loc_path_test = 'srv/{0}{1}'.format(self.appname, self.request_path)
   138    133           if self.test_local_file_exists(loc_path_test):
   139    134               return True
   140    135   
   141         -        log("[{0}] Try to find a candidate in the user defined routes".format(self.request_path))
          136  +        logging.debug("[{0}] Try to find a candidate in the user defined routes".format(self.request_path))
   142    137   
   143    138           for route in self.extract_interesting_routes():
   144    139               if self.test_method_exists(route):
   145    140                   return True
   146    141   
   147    142               route = 'srv/{0}/{1}'.format(self.appname, route)
   148    143               if self.test_local_file_exists(route):
................................................................................
   150    145   
   151    146           return False
   152    147   
   153    148   
   154    149       def preprocess_content(self):
   155    150           success = True
   156    151           if self.resource_is_method:
   157         -            log("[{0}] Spawn custom content method".format(self.resource_path))
          152  +            logging.debug("[{0}] Spawn custom content method".format(self.resource_path))
   158    153               self.content = getattr(self.App, self.resource_path)()
   159    154   
   160    155               if self.content == None:
   161    156                   success = False
   162    157   
   163    158           self.guess_content_type()
   164    159           return success
................................................................................
   173    168                   shutil.copyfileobj(f, self.App.wfile)
   174    169   
   175    170   
   176    171   
   177    172   class IcanDoThat(BaseHTTPRequestHandler):
   178    173       """ Main HTTP handler """
   179    174       server_version = '0xGA/0.1'
          175  +    appname = 'DEFAULT'
   180    176   
   181    177       def __init__(self, request, client_address, server):
   182    178           self.error = False
   183    179           self.extension = None
   184    180           self.current_domain = server.server_name
   185    181           self.current_port = server.server_port
   186    182   
   187    183           BaseHTTPRequestHandler.__init__(self, request, client_address, server)
   188    184   
   189    185   
   190    186       def load_config(self):
   191    187           host_infos = self.headers.get('host','').split(':')
   192    188           self.current_domain = host_infos[0]
   193         -        log('Request for {0} processed by {1}'.format(
          189  +        logging.debug('Request for {0} processed by {1}'.format(
   194    190               self.current_domain,
   195    191               threading.currentThread().getName()))
   196    192   
   197    193           config = ConfigParser()
   198    194           config.read('etc/rc.conf')
   199    195   
   200    196           if not self.current_domain in config:
   201    197               self.error = 'Domain is not reachable in configuration file'
   202    198   
   203    199           else:
   204    200               self.config = config[self.current_domain]
   205         -            global NARV_DEBUG
   206         -            NARV_DEBUG = self.config.getboolean('debug')
          201  +            logging.debug('Request is for App {0}'.format(self.appname))
   207    202   
   208         -            self.appname = None
   209         -            if 'appname' in self.config:
   210         -                self.appname = self.config['appname']
   211         -                log('Request is for App {0}'.format(self.appname))
   212    203   
   213         -            else:
   214         -                self.error = 'No claimed application in config file'
          204  +    def log_message(self, message, *args):
          205  +        logging.info(message % args)
   215    206   
   216    207   
   217    208       def do_error_page(self, errno, reason):
   218    209           self.build_headers(int(errno), 'text/html; charset=utf-8')
   219    210   
   220    211           if errno in self.config:
   221    212               with open(self.config[errno], 'rb') as f:
................................................................................
   248    239   </div>
   249    240   </body>
   250    241   </html>""".format(errno, reason)
   251    242               self.wfile.write(bytes(error_html, 'utf-8'))
   252    243   
   253    244   
   254    245       def do_404(self, reason='File not found'):
   255         -        log('[Error 404] {0}'.format(reason), True)
          246  +        logging.error('[Error 404] {0}'.format(reason))
   256    247           self.do_error_page('404', reason)
   257    248   
   258    249   
   259    250       def do_501(self, reason='Lack of configuration'):
   260         -        log('[Error 501] {0}'.format(reason), True)
          251  +        logging.critical('[Error 501] {0}'.format(reason))
   261    252           self.do_error_page('501', reason)
   262    253   
   263    254   
   264    255       def build_headers(self, resno, content_type = 'text/plain'):
   265    256           self.send_response(resno)
   266    257           if content_type != None:
   267    258               self.send_header('Content-Type', content_type)
................................................................................
   310    301   class NarvThreadedServer(ThreadingMixIn, HTTPServer):
   311    302       """Main threaded server"""
   312    303   
   313    304   
   314    305   class Narv:
   315    306       def __init__(self, server_infos=('', 8000), request_handler=IcanDoThat):
   316    307   
          308  +        config = ConfigParser()
          309  +        config.read('etc/rc.conf')
          310  +
          311  +        log_level = 'info'
          312  +        drop_priviledges = True
          313  +        narv_root_path = os.path.normpath(os.path.join(
          314  +            os.path.dirname(os.path.abspath(__file__)),
          315  +            '../../'))
          316  +
          317  +        if 'general' in config:
          318  +            if 'debug' in config['general']:
          319  +                log_level = config['general']['debug']
          320  +
          321  +            if 'drop_priviledges' in config['general']:
          322  +                drop_priviledges = config['general'].getboolean('drop_priviledges')
          323  +
          324  +        log_file = '{0}.{1}.log'.format(
          325  +            request_handler.appname,
          326  +            date.today().strftime("%Y%m%d")
          327  +        )
          328  +        logging.basicConfig(
          329  +            format='%(asctime)s -- [%(levelname)s] %(message)s',
          330  +            datefmt='%Y-%m-%d %H:%M:%S',
          331  +            filename=os.path.join(narv_root_path, 'var/log/', log_file),
          332  +            level=getattr(logging, log_level.upper()))
          333  +
   317    334           signal.signal(signal.SIGINT, self.shutdown)
   318    335           signal.signal(signal.SIGTERM, self.shutdown)
   319    336   
   320    337           self.server = NarvThreadedServer(server_infos, request_handler)
   321    338           server_thread = threading.Thread(target=self.server.serve_forever)
   322    339           server_thread.daemon = True
   323    340           server_thread.start()
   324    341   
   325         -        narv_root_path = os.path.normpath(os.path.join(
   326         -            os.path.dirname(os.path.abspath(__file__)),
   327         -            '../../'))
   328         -
   329    342           if os.getuid() == 0:
   330    343               os.chroot(narv_root_path)
   331    344               os.chdir("/")
   332         -            log("Dropping priviledges to UID {0} GID {1}".format('nobody', 'nobody'))
          345  +            logging.debug("Dropping priviledges to UID {0} GID {1}".format('nobody', 'nobody'))
   333    346   
   334    347               # Remove group privileges
   335    348               os.setgroups([])
   336    349   
   337    350               # Try setting the new uid/gid
   338    351               os.setgid(grp.getgrnam('nobody').gr_gid)
   339    352               os.setuid(pwd.getpwnam('nobody').pw_uid)
................................................................................
   341    354               # Ensure a very conservative umask
   342    355               os.umask(int('077', 8))
   343    356           else:
   344    357               os.chdir(narv_root_path)
   345    358   
   346    359   
   347    360       def start(self):
   348         -        log("Serving {0} at port {1}".format(self.server.server_name, self.server.server_port))
          361  +        logging.info("Serving {0} at port {1}".format(self.server.server_name, self.server.server_port))
   349    362           self.server.serve_forever()
   350    363   
   351    364   
   352    365       def shutdown(self, signal, frame):
   353         -        log("\nKthxbye", True)
          366  +        logging.info("Kthxbye")
   354    367           self.server.shutdown()
   355    368           sys.exit(0)
   356    369   
   357    370   
   358    371   
   359    372   if __name__ == "__main__":
   360    373       app = Narv()
   361    374       app.start()