[server] add a db-namespace option in source definition (closes #1631339)

cubicweb now depends on logilab-database >= 1.6.0

authorAdrien Di Mascio <Adrien.DiMascio@logilab.fr>
changesetec6b0763ef43
brancholdstable
phasedraft
hiddenyes
parent revision#dbffb6959564 server/source/native: fix wrong usage of .lstrip that produce garbled error messages (closes #2777641)
child revision<not specified>
files modified by this revision
server/serverctl.py
server/sources/native.py
server/sqlutils.py
# HG changeset patch
# User Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
# Date 1364495086 -3600
# Thu Mar 28 19:24:46 2013 +0100
# Branch oldstable
# Node ID ec6b0763ef436975f4d92fd9ccdc57bb09d9207e
# Parent dbffb6959564baed44c96b322e86da80f1858030
[server] add a db-namespace option in source definition (closes #1631339)

cubicweb now depends on logilab-database >= 1.6.0

diff --git a/server/serverctl.py b/server/serverctl.py
@@ -14,25 +14,29 @@
1  # details.
2  #
3  # You should have received a copy of the GNU Lesser General Public License along
4  # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
5  """cubicweb-ctl commands and command handlers specific to the repository"""
6 +from __future__ import with_statement
7 
8  __docformat__ = 'restructuredtext en'
9 
10  # *ctl module should limit the number of import to be imported as quickly as
11  # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
12  # completion). So import locally in command helpers.
13  import sys
14  import os
15 +from contextlib import contextmanager
16  import logging
17  import subprocess
18 
19  from logilab.common import nullobject
20  from logilab.common.configuration import Configuration
21  from logilab.common.shellutils import ASK, generate_password
22 
23 +from logilab.database import get_db_helper, get_connection
24 +
25  from cubicweb import AuthenticationError, ExecutionError, ConfigurationError
26  from cubicweb.toolsutils import Command, CommandHandler, underline_title
27  from cubicweb.cwctl import CWCTL, check_options_consistency
28  from cubicweb.server import SOURCE_TYPES
29  from cubicweb.server.repository import Repository
@@ -45,11 +49,10 @@
30  def source_cnx(source, dbname=None, special_privs=False, interactive=True):
31      """open and return a connection to the system database defined in the
32      given server.serverconfig
33      """
34      from getpass import getpass
35 -    from logilab.database import get_connection, get_db_helper
36      dbhost = source.get('db-host')
37      if dbname is None:
38          dbname = source['db-name']
39      driver = source['db-driver']
40      dbhelper = get_db_helper(driver)
@@ -84,10 +87,11 @@
41          user = password = None
42      extra_args = source.get('db-extra-arguments')
43      extra = extra_args and {'extra_args': extra_args} or {}
44      cnx = get_connection(driver, dbhost, dbname, user, password=password,
45                           port=source.get('db-port'),
46 +                         schema=source.get('db-namespace'),
47                           **extra)
48      try:
49          cnx.logged_user = user
50      except AttributeError:
51          # C object, __slots__
@@ -102,11 +106,10 @@
52      defined in the given config. If <dbms_system_base> is True,
53      connect to the dbms system database instead (for task such as
54      create/drop the instance database)
55      """
56      if dbms_system_base:
57 -        from logilab.database import get_db_helper
58          system_db = get_db_helper(source['db-driver']).system_database()
59          return source_cnx(source, system_db, special_privs=special_privs,
60                            interactive=interactive)
61      return source_cnx(source, special_privs=special_privs,
62                        interactive=interactive)
@@ -114,11 +117,10 @@
63  def _db_sys_cnx(source, special_privs, interactive=True):
64      """return a connection on the RDMS system table (to create/drop a user or a
65      database)
66      """
67      import logilab.common as lgp
68 -    from logilab.database import get_db_helper
69      lgp.USE_MX_DATETIME = False
70      driver = source['db-driver']
71      helper = get_db_helper(driver)
72      # connect on the dbms system base to create our base
73      cnx = system_source_cnx(source, True, special_privs=special_privs,
@@ -201,60 +203,85 @@
74                         self.config.appid])
75          else:
76              print ('-> nevermind, you can do it later with '
77                     '"cubicweb-ctl db-create %s".' % self.config.appid)
78 
79 -ERROR = nullobject()
80 -
81 -def confirm_on_error_or_die(msg, func, *args, **kwargs):
82 +@contextmanager
83 +def db_privilege_transaction(source, privilege):
84 +    print 'connecting to database %(db-name)s' % source
85 +    cnx = _db_sys_cnx(source, privilege)
86 +    cursor = cnx.cursor()
87      try:
88 -        return func(*args, **kwargs)
89 -    except Exception, ex:
90 -        print 'ERROR', ex
91 -        if not ASK.confirm('An error occurred while %s. Continue anyway?' % msg):
92 -            raise ExecutionError(str(ex))
93 -    return ERROR
94 +        yield cursor
95 +    except:
96 +        cnx.rollback()
97 +        cnx.close()
98 +        raise
99 +    else:
100 +        cnx.commit()
101 +        cnx.close()
102 
103  class RepositoryDeleteHandler(CommandHandler):
104      cmdname = 'delete'
105      cfgname = 'repository'
106 
107 +    def _drop_namespace(self, source):
108 +        db_namespace = source.get('db-namespace')
109 +        with db_privilege_transaction(source, privilege='DROP SCHEMA') as cursor:
110 +            helper = get_db_helper(source['db-driver'])
111 +            helper.drop_schema(cursor, db_namespace)
112 +            print '-> database schema %s dropped' % db_namespace
113 +
114 +    def _drop_database(self, source):
115 +        dbname = source['db-name']
116 +        if source['db-driver'] == 'sqlite':
117 +            print 'deleting database file %(db-name)s' % source
118 +            os.unlink(source['db-name'])
119 +            print '-> database %(db-name)s dropped.' % source
120 +        else:
121 +            helper = get_db_helper(source['db-driver'])
122 +            with db_privilege_transaction(source, privilege='DROP DATABASE') as cursor:
123 +                print 'dropping database %(db-name)s' % source
124 +                cursor.execute('DROP DATABASE "%(db-name)s"')
125 +                print '-> database %(db-name)s dropped.' % dbname
126 +
127 +    def _drop_user(self, source):
128 +        user = source['db-user'] or None
129 +        if user is not None:
130 +            with db_privilege_transaction(source, privilege='DROP USER') as cursor:
131 +                print 'dropping user %s' % user
132 +                cursor.execute('DROP USER %s' % user)
133 +
134 +    def _cleanup_steps(self, source):
135 +        # 1/ delete namespace if used
136 +        db_namespace = source.get('db-namespace')
137 +        if db_namespace:
138 +            yield ('Delete database namespace "%s"' % db_namespace,
139 +                   self._drop_namespace, True)
140 +        # 2/ delete database
141 +        yield ('Delete database "%(db-name)s"' % source,
142 +               self._drop_database, True)
143 +        # 3/ delete user
144 +        helper = get_db_helper(source['db-driver'])
145 +        if source['db-user'] and helper.users_support:
146 +            # XXX should check we are not connected as user
147 +            yield ('Delete user "%(db-user)s"' % source,
148 +                   self._drop_user, False)
149 +
150      def cleanup(self):
151          """remove instance's configuration and database"""
152 -        from logilab.database import get_db_helper
153          source = self.config.sources()['system']
154 -        dbname = source['db-name']
155 -        helper = get_db_helper(source['db-driver'])
156 -        if ASK.confirm('Delete database %s ?' % dbname):
157 -            if source['db-driver'] == 'sqlite':
158 -                if confirm_on_error_or_die(
159 -                    'deleting database file %s' % dbname,
160 -                    os.unlink, source['db-name']) is not ERROR:
161 -                    print '-> database %s dropped.' % dbname
162 -                return
163 -            user = source['db-user'] or None
164 -            cnx = confirm_on_error_or_die('connecting to database %s' % dbname,
165 -                                          _db_sys_cnx, source, 'DROP DATABASE')
166 -            if cnx is ERROR:
167 -                return
168 -            cursor = cnx.cursor()
169 -            try:
170 -                if confirm_on_error_or_die(
171 -                    'dropping database %s' % dbname,
172 -                    cursor.execute, 'DROP DATABASE "%s"' % dbname) is not ERROR:
173 -                    print '-> database %s dropped.' % dbname
174 -                # XXX should check we are not connected as user
175 -                if user and helper.users_support and \
176 -                       ASK.confirm('Delete user %s ?' % user, default_is_yes=False):
177 -                    if confirm_on_error_or_die(
178 -                        'dropping user %s' % user,
179 -                        cursor.execute, 'DROP USER %s' % user) is not ERROR:
180 -                        print '-> user %s dropped.' % user
181 -                cnx.commit()
182 -            except BaseException:
183 -                cnx.rollback()
184 -                raise
185 +        for msg, step, default in self._cleanup_steps(source):
186 +            if ASK.confirm(msg, default_is_yes=default):
187 +                try:
188 +                    step(source)
189 +                except Exception, exc:
190 +                    print 'ERROR', exc
191 +                    if ASK.confirm('An error occurred. Continue anyway?',
192 +                                   default_is_yes=False):
193 +                        continue
194 +                    raise ExecutionError(str(exc))
195 
196 
197  class RepositoryStartHandler(CommandHandler):
198      cmdname = 'start'
199      cfgname = 'repository'
@@ -290,10 +317,11 @@
200                                 source['db-encoding'], **kwargs)
201      else:
202          helper.create_database(cursor, source['db-name'],
203                                 dbencoding=source['db-encoding'], **kwargs)
204 
205 +
206  class CreateInstanceDBCommand(Command):
207      """Create the system database of an instance (run after 'create').
208 
209      You will be prompted for a login / password to use to connect to
210      the system database.  The given user should have almost all rights
@@ -327,11 +355,10 @@
211            }),
212          )
213 
214      def run(self, args):
215          """run the command with its specific arguments"""
216 -        from logilab.database import get_db_helper
217          check_options_consistency(self.config)
218          automatic = self.get('automatic')
219          appid = args.pop()
220          config = ServerConfiguration.config_for(appid)
221          source = config.sources()['system']
@@ -371,10 +398,14 @@
222                  raise
223          cnx = system_source_cnx(source, special_privs='CREATE LANGUAGE',
224                                  interactive=not automatic)
225          cursor = cnx.cursor()
226          helper.init_fti_extensions(cursor)
227 +        namespace = source.get('db-namespace')
228 +        if namespace and ASK.confirm('Create schema %s in database %s ?'
229 +                                     % (namespace, dbname)):
230 +            helper.create_schema(cursor, namespace)
231          cnx.commit()
232          # postgres specific stuff
233          if driver == 'postgres':
234              # install plpythonu/plpgsql languages
235              langs = ('plpythonu', 'plpgsql')
@@ -435,22 +466,21 @@
236 
237      def run(self, args):
238          check_options_consistency(self.config)
239          print '\n'+underline_title('Initializing the system database')
240          from cubicweb.server import init_repository
241 -        from logilab.database import get_connection
242          appid = args[0]
243          config = ServerConfiguration.config_for(appid)
244          try:
245              system = config.sources()['system']
246              extra_args=system.get('db-extra-arguments')
247              extra = extra_args and {'extra_args': extra_args} or {}
248              get_connection(
249                  system['db-driver'], database=system['db-name'],
250                  host=system.get('db-host'), port=system.get('db-port'),
251                  user=system.get('db-user') or '', password=system.get('db-password') or '',
252 -                **extra)
253 +                schema=system.get('db-namespace'), **extra)
254          except Exception, ex:
255              raise ConfigurationError(
256                  'You seem to have provided wrong connection information in '\
257                  'the %s file. Resolve this first (error: %s).'
258                  % (config.sources_file(), str(ex).strip()))
@@ -592,11 +622,10 @@
259          except KeyError:
260              print '-> Error: could not get cubicweb administrator login.'
261              sys.exit(1)
262          cnx = source_cnx(sourcescfg['system'])
263          driver = sourcescfg['system']['db-driver']
264 -        from logilab.database import get_db_helper
265          dbhelper = get_db_helper(driver)
266          cursor = cnx.cursor()
267          # check admin exists
268          cursor.execute("SELECT * FROM cw_CWUser WHERE cw_login=%(l)s",
269                         {'l': adminlogin})
diff --git a/server/sources/native.py b/server/sources/native.py
@@ -253,10 +253,16 @@
270           {'type' : 'string',
271            'default': Method('default_instance_id'),
272            'help': 'database name',
273            'group': 'native-source', 'level': 0,
274            }),
275 +        ('db-namespace',
276 +         {'type' : 'string',
277 +          'default': '',
278 +          'help': 'database namespace (schema) name',
279 +          'group': 'native-source', 'level': 1,
280 +          }),
281          ('db-user',
282           {'type' : 'string',
283            'default': CubicWebNoAppConfiguration.mode == 'user' and getlogin() or 'cubicweb',
284            'help': 'database user',
285            'group': 'native-source', 'level': 0,
diff --git a/server/sqlutils.py b/server/sqlutils.py
@@ -153,14 +153,15 @@
286          dbport = port and int(port) or None
287          dbuser = source_config.get('db-user')
288          dbpassword = source_config.get('db-password')
289          dbencoding = source_config.get('db-encoding', 'UTF-8')
290          dbextraargs = source_config.get('db-extra-arguments')
291 +        dbnamespace = source_config.get('db-namespace')
292          self.dbhelper = db.get_db_helper(self.dbdriver)
293          self.dbhelper.record_connection_info(dbname, dbhost, dbport, dbuser,
294                                               dbpassword, dbextraargs,
295 -                                             dbencoding)
296 +                                             dbencoding, dbnamespace)
297          self.sqlgen = SQLGenerator()
298          # copy back some commonly accessed attributes
299          dbapi_module = self.dbhelper.dbapi_module
300          self.OperationalError = dbapi_module.OperationalError
301          self.InterfaceError = dbapi_module.InterfaceError