goa/tools/laxctl.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 28 Apr 2010 10:06:01 +0200
branchstable
changeset 5421 8167de96c523
parent 4252 6c4f109c2b03
child 5424 8ecbcbff9777
permissions -rw-r--r--
proper licensing information (LGPL-2.1). Hope I get it right this time.

# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# logilab-common is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
"""provides all lax instances management commands into a single utility script

"""
__docformat__ = "restructuredtext en"

import sys
import os
import os.path as osp
import time
import re
import urllib2
from urllib import urlencode
from Cookie import SimpleCookie

from logilab.common.clcommands import Command, register_commands, main_run

from cubicweb.uilib import remove_html_tags
from cubicweb.web.views.schema import SKIP_TYPES

APPLROOT = osp.abspath(osp.join(osp.dirname(osp.abspath(__file__)), '..'))


def initialize_vregistry(applroot):
    # apply monkey patches first
    from cubicweb.goa import do_monkey_patch
    do_monkey_patch()
    from cubicweb.goa.goavreg import GAEVregistry
    from cubicweb.goa.goaconfig import GAEConfiguration
    #WebConfiguration.ext_resources['JAVASCRIPTS'].append('DATADIR/goa.js')
    config = GAEConfiguration('toto', applroot)
    vreg = GAEVregistry(config)
    vreg.set_schema(config.load_schema())
    return vreg

def alistdir(directory):
    return [osp.join(directory, f) for f in os.listdir(directory)]


class LaxCommand(Command):
    """base command class for all lax commands
    creates vreg, schema and calls
    """
    min_args = max_args = 0

    def run(self, args):
        self.vreg = initialize_vregistry(APPLROOT)
        self._run(args)


class GenerateSchemaCommand(LaxCommand):
    """generates the schema's png file"""
    name = 'genschema'

    def _run(self, args):
        assert not args, 'no argument expected'
        from yams import schema2dot
        schema = self.vreg.schema
        path = osp.join(APPLROOT, 'data', 'schema.png')
        schema2dot.schema2dot(schema, path, #size=size,
                              skiptypes=SKIP_TYPES)
        print 'generated', path
        path = osp.join(APPLROOT, 'data', 'metaschema.png')
        schema2dot.schema2dot(schema, path)
        print 'generated', path


class PopulateDataDirCommand(LaxCommand):
    """populate instance's data directory according to used cubes"""
    name = 'populatedata'

    def _run(self, args):
        assert not args, 'no argument expected'
        # first clean everything which is a symlink from the data directory
        datadir = osp.join(APPLROOT, 'data')
        if not osp.exists(datadir):
            print 'created data directory'
            os.mkdir(datadir)
        for filepath in alistdir(datadir):
            if osp.islink(filepath):
                print 'removing', filepath
                os.remove(filepath)
        cubes = list(self.vreg.config.cubes()) + ['shared']
        for templ in cubes:
            templpath = self.vreg.config.cube_dir(templ)
            templdatadir = osp.join(templpath, 'data')
            if not osp.exists(templdatadir):
                print 'no data provided by', templ
                continue
            for resource in os.listdir(templdatadir):
                if resource == 'external_resources':
                    continue
                if not osp.exists(osp.join(datadir, resource)):
                    print 'symlinked %s from %s' % (resource, templ)
                    os.symlink(osp.join(templdatadir, resource),
                               osp.join(datadir, resource))


class NoRedirectHandler(urllib2.HTTPRedirectHandler):
    def http_error_302(self, req, fp, code, msg, headers):
        raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
    http_error_301 = http_error_303 = http_error_307 = http_error_302


class GetSessionIdHandler(urllib2.HTTPRedirectHandler):
    def __init__(self, config):
        self.config = config

    def http_error_303(self, req, fp, code, msg, headers):
        cookie = SimpleCookie(headers['Set-Cookie'])
        sessionid = cookie['__session'].value
        print 'session id', sessionid
        setattr(self.config, 'cookie', '__session=' + sessionid)
        return 1 # on exception should be raised


class URLCommand(LaxCommand):
    """abstract class for commands doing stuff by accessing the web instance
    """
    min_args = max_args = 1
    arguments = '<site url>'

    options = (
        ('cookie',
         {'short': 'C', 'type' : 'string', 'metavar': 'key=value',
          'default': None,
          'help': 'session/authentication cookie.'}),
        ('user',
         {'short': 'u', 'type' : 'string', 'metavar': 'login',
          'default': None,
          'help': 'user login instead of giving raw cookie string (require lax '
          'based authentication).'}),
        ('password',
         {'short': 'p', 'type' : 'string', 'metavar': 'password',
          'default': None,
          'help': 'user password instead of giving raw cookie string (require '
          'lax based authentication).'}),
        )

    def _run(self, args):
        baseurl = args[0]
        if not baseurl.startswith('http'):
            baseurl = 'http://' + baseurl
        if not baseurl.endswith('/'):
            baseurl += '/'
        self.base_url = baseurl
        if not self.config.cookie and self.config.user:
            # no cookie specified but a user is. Try to open a session using
            # given authentication info
            print 'opening session for', self.config.user
            opener = urllib2.build_opener(GetSessionIdHandler(self.config))
            urllib2.install_opener(opener)
            data = urlencode(dict(__login=self.config.user,
                                  __password=self.config.password))
            self.open_url(urllib2.Request(baseurl, data))
        opener = urllib2.build_opener(NoRedirectHandler())
        urllib2.install_opener(opener)
        self.do_base_url(baseurl)

    def build_req(self, url):
        req = urllib2.Request(url)
        if self.config.cookie:
            req.headers['Cookie'] = self.config.cookie
        return req

    def open_url(self, req):
        try:
            return urllib2.urlopen(req)
        except urllib2.HTTPError, ex:
            if ex.code == 302:
                self.error_302(req, ex)
            elif ex.code == 500:
                self.error_500(req, ex)
            else:
                raise

    def error_302(self, req, ex):
        print 'authentication required'
        print ('visit %s?vid=authinfo with your browser to get '
               'authentication info' % self.base_url)
        sys.exit(1)

    def error_500(self, req, ex):
        print 'an unexpected error occured on the server'
        print ('you may get more information by visiting '
               '%s' % req.get_full_url())
        sys.exit(1)

    def extract_message(self, data):
        match = re.search(r'<div class="message">(.*?)</div>', data.read(), re.M|re.S)
        if match:
            msg = remove_html_tags(match.group(1))
            print msg
            return msg

    def do_base_url(self, baseurl):
        raise NotImplementedError()


class DSInitCommand(URLCommand):
    """initialize the datastore"""
    name = 'db-init'

    options = URLCommand.options + (
        ('sleep',
         {'short': 's', 'type' : 'int', 'metavar': 'nb seconds',
          'default': None,
          'help': 'number of seconds to wait between each request to avoid '
          'going out of quota.'}),
        )

    def do_base_url(self, baseurl):
        req = self.build_req(baseurl + '?vid=contentinit')
        while True:
            try:
                data = self.open_url(req)
            except urllib2.HTTPError, ex:
                if ex.code == 303: # redirect
                    print 'process completed'
                    break
                raise
            msg = self.extract_message(data)
            if msg and msg.startswith('error: '):
                print ('you may to cleanup datastore by visiting '
                       '%s?vid=contentclear (ALL ENTITIES WILL BE DELETED)'
                       % baseurl)
                break
            if self.config.sleep:
                time.sleep(self.config.sleep)


class CleanSessionsCommand(URLCommand):
    """cleanup sessions on the server. This command should usually be called
    regularly by a cron job or equivalent.
    """
    name = "cleansessions"
    def do_base_url(self, baseurl):
        req = self.build_req(baseurl + '?vid=cleansessions')
        data = self.open_url(req)
        self.extract_message(data)


register_commands([GenerateSchemaCommand,
                   PopulateDataDirCommand,
                   DSInitCommand,
                   CleanSessionsCommand,
                   ])

def run():
    main_run(sys.argv[1:])

if __name__ == '__main__':
    run()