opensipscli 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opensipscli/__init__.py +20 -0
- opensipscli/args.py +56 -0
- opensipscli/cli.py +472 -0
- opensipscli/comm.py +57 -0
- opensipscli/config.py +162 -0
- opensipscli/db.py +989 -0
- opensipscli/defaults.py +91 -0
- opensipscli/libs/__init__.py +20 -0
- opensipscli/libs/sqlalchemy_utils.py +244 -0
- opensipscli/logger.py +85 -0
- opensipscli/main.py +86 -0
- opensipscli/module.py +69 -0
- opensipscli/modules/__init__.py +24 -0
- opensipscli/modules/database.py +1062 -0
- opensipscli/modules/diagnose.py +1089 -0
- opensipscli/modules/instance.py +53 -0
- opensipscli/modules/mi.py +200 -0
- opensipscli/modules/tls.py +354 -0
- opensipscli/modules/trace.py +292 -0
- opensipscli/modules/trap.py +138 -0
- opensipscli/modules/user.py +281 -0
- opensipscli/version.py +22 -0
- opensipscli-0.3.1.data/scripts/opensips-cli +9 -0
- opensipscli-0.3.1.dist-info/LICENSE +674 -0
- opensipscli-0.3.1.dist-info/METADATA +225 -0
- opensipscli-0.3.1.dist-info/RECORD +28 -0
- opensipscli-0.3.1.dist-info/WHEEL +5 -0
- opensipscli-0.3.1.dist-info/top_level.txt +1 -0
opensipscli/db.py
ADDED
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
##
|
|
3
|
+
## This file is part of OpenSIPS CLI
|
|
4
|
+
## (see https://github.com/OpenSIPS/opensips-cli).
|
|
5
|
+
##
|
|
6
|
+
## This program is free software: you can redistribute it and/or modify
|
|
7
|
+
## it under the terms of the GNU General Public License as published by
|
|
8
|
+
## the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
## (at your option) any later version.
|
|
10
|
+
##
|
|
11
|
+
## This program is distributed in the hope that it will be useful,
|
|
12
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
## GNU General Public License for more details.
|
|
15
|
+
##
|
|
16
|
+
## You should have received a copy of the GNU General Public License
|
|
17
|
+
## along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
##
|
|
19
|
+
|
|
20
|
+
from opensipscli.logger import logger
|
|
21
|
+
from opensipscli.config import cfg
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import sqlalchemy
|
|
26
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
27
|
+
from sqlalchemy import Column, Date, Integer, String, Boolean
|
|
28
|
+
from sqlalchemy.orm import sessionmaker, deferred
|
|
29
|
+
|
|
30
|
+
# for now, we use our own make_url(), since Alchemy API is highly unstable
|
|
31
|
+
# (https://github.com/OpenSIPS/opensips-cli/issues/85)
|
|
32
|
+
#from sqlalchemy.engine.url import make_url
|
|
33
|
+
|
|
34
|
+
sqlalchemy_available = True
|
|
35
|
+
logger.debug("SQLAlchemy version: ", sqlalchemy.__version__)
|
|
36
|
+
try:
|
|
37
|
+
import sqlalchemy_utils
|
|
38
|
+
except ImportError:
|
|
39
|
+
logger.debug("using embedded implementation of SQLAlchemy_Utils")
|
|
40
|
+
# copied from SQLAlchemy_utils repository
|
|
41
|
+
from opensipscli.libs import sqlalchemy_utils
|
|
42
|
+
except ImportError:
|
|
43
|
+
logger.info("sqlalchemy not available!")
|
|
44
|
+
sqlalchemy_available = False
|
|
45
|
+
|
|
46
|
+
SUPPORTED_BACKENDS = [
|
|
47
|
+
"mysql",
|
|
48
|
+
"postgresql",
|
|
49
|
+
"sqlite",
|
|
50
|
+
"oracle",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
SQLAlchemy: Classes for ORM handling
|
|
55
|
+
"""
|
|
56
|
+
if sqlalchemy_available:
|
|
57
|
+
Base = declarative_base()
|
|
58
|
+
|
|
59
|
+
class Roles(Base):
|
|
60
|
+
"""
|
|
61
|
+
Postgres: Roles database
|
|
62
|
+
"""
|
|
63
|
+
__tablename__ = 'pg_roles'
|
|
64
|
+
|
|
65
|
+
oid = Column(Integer, primary_key=True)
|
|
66
|
+
rolname = Column(String)
|
|
67
|
+
rolsuper = deferred(Column(Boolean), group='options')
|
|
68
|
+
rolinherit = deferred(Column(Boolean), group='options')
|
|
69
|
+
rolcreaterole = deferred(Column(Boolean), group='options')
|
|
70
|
+
rolcreatedb = deferred(Column(Boolean), group='options')
|
|
71
|
+
rolcanlogin = deferred(Column(Boolean), group='options')
|
|
72
|
+
rolreplication = deferred(Column(Boolean), group='options')
|
|
73
|
+
rolconnlimit = deferred(Column(Integer))
|
|
74
|
+
rolpassword = Column(String)
|
|
75
|
+
rolvaliduntil = deferred(Column(Date))
|
|
76
|
+
rolbypassrls = deferred(Column(Boolean))
|
|
77
|
+
rolconfig = deferred(Column(String))
|
|
78
|
+
|
|
79
|
+
def __repr__(self):
|
|
80
|
+
"""
|
|
81
|
+
returns a string from an arbitrary object
|
|
82
|
+
"""
|
|
83
|
+
return self.shape
|
|
84
|
+
|
|
85
|
+
class osdbError(Exception):
|
|
86
|
+
"""
|
|
87
|
+
OSDB: error handler
|
|
88
|
+
"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
class osdbConnectError(osdbError):
|
|
92
|
+
"""
|
|
93
|
+
OSDB: connecton error handler
|
|
94
|
+
"""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
class osdbArgumentError(osdbError):
|
|
98
|
+
"""
|
|
99
|
+
OSDB: argument error handler
|
|
100
|
+
"""
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
class osdbNoSuchModuleError(osdbError):
|
|
104
|
+
"""
|
|
105
|
+
OSDB: module error handler
|
|
106
|
+
"""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
class osdbModuleAlreadyExistsError(osdbError):
|
|
110
|
+
"""
|
|
111
|
+
OSDB: module error handler
|
|
112
|
+
"""
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
class osdbAccessDeniedError(osdbError):
|
|
116
|
+
"""
|
|
117
|
+
OSDB: module error handler
|
|
118
|
+
"""
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
class DBURL(object):
|
|
122
|
+
@staticmethod
|
|
123
|
+
def escape_pass(pwd):
|
|
124
|
+
for sym in ('@', '/'): # special symbols accepted in password
|
|
125
|
+
pwd = pwd.replace(sym, '%'+hex(ord('@'))[2:])
|
|
126
|
+
return pwd
|
|
127
|
+
|
|
128
|
+
def __init__(self, url):
|
|
129
|
+
arr = url.split('://')
|
|
130
|
+
self.drivername = arr[0].strip()
|
|
131
|
+
|
|
132
|
+
if len(arr) != 2 or not self.drivername:
|
|
133
|
+
raise Exception('Failed to parse RFC 1738 URL')
|
|
134
|
+
|
|
135
|
+
self.username = None
|
|
136
|
+
self.password = None
|
|
137
|
+
self.host = None
|
|
138
|
+
self.port = None
|
|
139
|
+
self.database = None
|
|
140
|
+
|
|
141
|
+
url = arr[1].strip()
|
|
142
|
+
if not url:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
arr = url.split('/')
|
|
146
|
+
if len(arr) > 1:
|
|
147
|
+
self.database = "/".join(arr[1:]).strip()
|
|
148
|
+
url = arr[0].strip()
|
|
149
|
+
|
|
150
|
+
arr = url.split('@')
|
|
151
|
+
if len(arr) > 1:
|
|
152
|
+
# handle user + password
|
|
153
|
+
upass = '@'.join(arr[:-1]).strip().split(':')
|
|
154
|
+
self.username = upass[0].strip()
|
|
155
|
+
if len(upass) > 1:
|
|
156
|
+
self.password = self.escape_pass(":".join(upass[1:]).strip())
|
|
157
|
+
url = arr[-1].strip()
|
|
158
|
+
else:
|
|
159
|
+
url = arr[0].strip()
|
|
160
|
+
|
|
161
|
+
# handle host + port
|
|
162
|
+
arr = url.strip().split(':')
|
|
163
|
+
self.host = arr[0].strip()
|
|
164
|
+
if len(arr) > 1:
|
|
165
|
+
self.port = int(arr[1].strip())
|
|
166
|
+
|
|
167
|
+
def __repr__(self):
|
|
168
|
+
return "{}://{}{}{}{}{}{}".format(
|
|
169
|
+
self.drivername,
|
|
170
|
+
self.username or "",
|
|
171
|
+
":***" if self.username != None and self.password != None else "",
|
|
172
|
+
"@" if self.username != None else "",
|
|
173
|
+
self.host or "",
|
|
174
|
+
":" + str(self.port) if self.port != None else "",
|
|
175
|
+
"/" + self.database if self.database != None else "")
|
|
176
|
+
|
|
177
|
+
def __str__(self):
|
|
178
|
+
return "{}://{}{}{}{}{}{}".format(
|
|
179
|
+
self.drivername,
|
|
180
|
+
self.username or "",
|
|
181
|
+
":" + self.password if self.username != None and self.password != None else "",
|
|
182
|
+
"@" if self.username != None else "",
|
|
183
|
+
self.host or "",
|
|
184
|
+
":" + str(self.port) if self.port != None else "",
|
|
185
|
+
"/" + self.database if self.database != None else "")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def make_url(url_string):
|
|
189
|
+
return DBURL(url_string)
|
|
190
|
+
|
|
191
|
+
class osdb(object):
|
|
192
|
+
"""
|
|
193
|
+
Class: object store database
|
|
194
|
+
"""
|
|
195
|
+
def __init__(self, db_url, db_name):
|
|
196
|
+
"""
|
|
197
|
+
constructor
|
|
198
|
+
"""
|
|
199
|
+
self.db_url = db_url
|
|
200
|
+
self.db_name = db_name
|
|
201
|
+
self.dialect = osdb.get_dialect(db_url)
|
|
202
|
+
self.Session = sessionmaker()
|
|
203
|
+
self.__engine = None
|
|
204
|
+
self.__conn = None
|
|
205
|
+
|
|
206
|
+
# TODO: do this only for SQLAlchemy
|
|
207
|
+
try:
|
|
208
|
+
if self.dialect == "postgresql":
|
|
209
|
+
self.__engine = sqlalchemy.create_engine(db_url, isolation_level='AUTOCOMMIT')
|
|
210
|
+
else:
|
|
211
|
+
self.__engine = sqlalchemy.create_engine(db_url)
|
|
212
|
+
|
|
213
|
+
logger.debug("connecting to %s", db_url)
|
|
214
|
+
self.__conn = self.__engine.connect().\
|
|
215
|
+
execution_options(autocommit=True)
|
|
216
|
+
# connect the Session object to our engine
|
|
217
|
+
self.Session.configure(bind=self.__engine)
|
|
218
|
+
# instanciate the Session object
|
|
219
|
+
self.__session = self.Session()
|
|
220
|
+
except sqlalchemy.exc.OperationalError as se:
|
|
221
|
+
if self.dialect == "mysql":
|
|
222
|
+
try:
|
|
223
|
+
if int(se.args[0].split(",")[0].split("(")[2]) in [
|
|
224
|
+
2006, # MySQL
|
|
225
|
+
1698, # MariaDB "Access Denied"
|
|
226
|
+
1044, # MariaDB "DB Access Denied"
|
|
227
|
+
1045, # MariaDB "Access Denied (Using Password)"
|
|
228
|
+
]:
|
|
229
|
+
raise osdbAccessDeniedError
|
|
230
|
+
except osdbAccessDeniedError:
|
|
231
|
+
raise
|
|
232
|
+
except:
|
|
233
|
+
logger.error("unexpected parsing exception")
|
|
234
|
+
elif self.dialect == "postgresql" and \
|
|
235
|
+
(("authentication" in se.args[0] and "failed" in se.args[0]) or \
|
|
236
|
+
("no password supplied" in se.args[0])):
|
|
237
|
+
raise osdbAccessDeniedError
|
|
238
|
+
|
|
239
|
+
raise osdbError("unable to connect to the database")
|
|
240
|
+
except sqlalchemy.exc.NoSuchModuleError:
|
|
241
|
+
raise osdbError("cannot handle {} dialect".
|
|
242
|
+
format(self.dialect))
|
|
243
|
+
except sqlalchemy.exc.ArgumentError:
|
|
244
|
+
raise osdbArgumentError("bad DB URL: {}".format(
|
|
245
|
+
self.db_url))
|
|
246
|
+
|
|
247
|
+
def alter_role(self, role_name, role_options=None, role_password=None):
|
|
248
|
+
"""
|
|
249
|
+
alter attributes of a role object
|
|
250
|
+
"""
|
|
251
|
+
# TODO: is any other dialect using the "role" concept?
|
|
252
|
+
if self.dialect != "postgresql":
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
# TODO: do this only for SQLAlchemy
|
|
256
|
+
if not self.__conn:
|
|
257
|
+
raise osdbError("connection not available")
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
if not role_options is None:
|
|
261
|
+
sqlcmd = "ALTER ROLE {} WITH {}".format(role_name, role_options)
|
|
262
|
+
msg = "Alter role '{}' with options '{}'". \
|
|
263
|
+
format(role_name, role_options, self.db_name)
|
|
264
|
+
if not role_password is None:
|
|
265
|
+
sqlcmd += " PASSWORD '{}'".format(role_password)
|
|
266
|
+
msg += " and password '********'"
|
|
267
|
+
msg += " on database '{}'".format(self.db_name)
|
|
268
|
+
try:
|
|
269
|
+
result = self.__conn.execute(sqlcmd)
|
|
270
|
+
if result:
|
|
271
|
+
logger.info( "{} was successfull".format(msg))
|
|
272
|
+
except:
|
|
273
|
+
logger.error("%s failed", msg)
|
|
274
|
+
return False
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
def connect(self, db_name=None):
|
|
278
|
+
"""
|
|
279
|
+
connect to database
|
|
280
|
+
"""
|
|
281
|
+
if db_name is not None:
|
|
282
|
+
self.db_name = db_name
|
|
283
|
+
# TODO: do this only for SQLAlchemy
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
if self.dialect == "postgresql":
|
|
287
|
+
self.db_url = self.set_url_db(self.db_url, self.db_name)
|
|
288
|
+
if sqlalchemy_utils.database_exists(self.db_url) is True:
|
|
289
|
+
engine = sqlalchemy.create_engine(self.db_url, isolation_level='AUTOCOMMIT')
|
|
290
|
+
if self.__conn:
|
|
291
|
+
self.__conn.close()
|
|
292
|
+
self.__conn = engine.connect()
|
|
293
|
+
# connect the Session object to our engine
|
|
294
|
+
self.Session.configure(bind=self.__engine)
|
|
295
|
+
# instanciate the Session object
|
|
296
|
+
self.session = self.Session()
|
|
297
|
+
logger.debug("connected to database URL '%s'", self.db_url)
|
|
298
|
+
elif self.dialect != "sqlite":
|
|
299
|
+
self.__conn.execute("USE {}".format(self.db_name))
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.error("failed to connect to %s", self.db_url)
|
|
302
|
+
logger.error(e)
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
def create(self, db_name=None):
|
|
308
|
+
"""
|
|
309
|
+
create a database object
|
|
310
|
+
"""
|
|
311
|
+
if db_name is None:
|
|
312
|
+
db_name = self.db_name
|
|
313
|
+
# TODO: do this only for SQLAlchemy
|
|
314
|
+
if not self.__conn:
|
|
315
|
+
raise osdbError("connection not available")
|
|
316
|
+
|
|
317
|
+
logger.debug("Create Database '%s' for dialect '%s' ...",
|
|
318
|
+
self.db_name, self.dialect)
|
|
319
|
+
|
|
320
|
+
# all good - it's time to create the database
|
|
321
|
+
if self.dialect == "postgresql":
|
|
322
|
+
self.__conn.connection.connection.set_isolation_level(0)
|
|
323
|
+
try:
|
|
324
|
+
self.__conn.execute("CREATE DATABASE {}".format(self.db_name))
|
|
325
|
+
self.__conn.connection.connection.set_isolation_level(1)
|
|
326
|
+
except sqlalchemy.exc.OperationalError as se:
|
|
327
|
+
logger.error("cannot create database: {}!".format(se))
|
|
328
|
+
return False
|
|
329
|
+
elif self.dialect != "sqlite":
|
|
330
|
+
self.__conn.execute("CREATE DATABASE {}".format(self.db_name))
|
|
331
|
+
|
|
332
|
+
logger.debug("success")
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
def create_module(self, import_file):
|
|
336
|
+
"""
|
|
337
|
+
create a module object
|
|
338
|
+
"""
|
|
339
|
+
self.exec_sql_file(import_file)
|
|
340
|
+
|
|
341
|
+
def ensure_user(self, db_url):
|
|
342
|
+
url = make_url(db_url)
|
|
343
|
+
if url.password is None:
|
|
344
|
+
logger.error("database URL does not include a password")
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
if url.drivername.lower() == "mysql":
|
|
348
|
+
sqlcmd = "CREATE USER IF NOT EXISTS '{}' IDENTIFIED BY '{}'".format(
|
|
349
|
+
url.username, url.password)
|
|
350
|
+
try:
|
|
351
|
+
result = self.__conn.execute(sqlcmd)
|
|
352
|
+
if result:
|
|
353
|
+
logger.info("created user '%s'", url.username)
|
|
354
|
+
except:
|
|
355
|
+
logger.error("failed to create user '%s'", url.username)
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
if url.username == 'root':
|
|
359
|
+
logger.debug("skipping password change for root user")
|
|
360
|
+
else:
|
|
361
|
+
"""
|
|
362
|
+
Query compatibility facts when changing a MySQL user password:
|
|
363
|
+
- SET PASSWORD syntax has diverged between MySQL and MariaDB
|
|
364
|
+
- ALTER USER syntax is not supported in MariaDB < 10.2
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
# try MariaDB syntax first
|
|
368
|
+
sqlcmd = "SET PASSWORD FOR '{}' = PASSWORD('{}')".format(
|
|
369
|
+
url.username, url.password)
|
|
370
|
+
try:
|
|
371
|
+
result = self.__conn.execute(sqlcmd)
|
|
372
|
+
if result:
|
|
373
|
+
logger.info("set password '%s%s%s' for '%s' (MariaDB)",
|
|
374
|
+
url.password[0] if len(url.password) >= 1 else '',
|
|
375
|
+
(len(url.password) - 2) * '*',
|
|
376
|
+
url.password[-1] if len(url.password) >= 2 else '',
|
|
377
|
+
url.username)
|
|
378
|
+
except sqlalchemy.exc.ProgrammingError as se:
|
|
379
|
+
try:
|
|
380
|
+
if int(se.args[0].split(",")[0].split("(")[2]) == 1064:
|
|
381
|
+
# syntax error! OK, now try Oracle MySQL syntax
|
|
382
|
+
sqlcmd = "ALTER USER '{}' IDENTIFIED BY '{}'".format(
|
|
383
|
+
url.username, url.password)
|
|
384
|
+
result = self.__conn.execute(sqlcmd)
|
|
385
|
+
if result:
|
|
386
|
+
logger.info("set password '%s%s%s' for '%s' (MySQL)",
|
|
387
|
+
url.password[0] if len(url.password) >= 1 else '',
|
|
388
|
+
(len(url.password) - 2) * '*',
|
|
389
|
+
url.password[-1] if len(url.password) >= 2 else '',
|
|
390
|
+
url.username)
|
|
391
|
+
except:
|
|
392
|
+
logger.exception("failed to set password for '%s'", url.username)
|
|
393
|
+
return False
|
|
394
|
+
except:
|
|
395
|
+
logger.exception("failed to set password for '%s'", url.username)
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
sqlcmd = "GRANT ALL ON {}.* TO '{}'".format(self.db_name, url.username)
|
|
399
|
+
try:
|
|
400
|
+
result = self.__conn.execute(sqlcmd)
|
|
401
|
+
if result:
|
|
402
|
+
logger.info("granted access to user '%s' on DB '%s'",
|
|
403
|
+
url.username, self.db_name)
|
|
404
|
+
except:
|
|
405
|
+
logger.exception("failed to grant access to '%s' on DB '%s'",
|
|
406
|
+
url.username, self.db_name)
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
sqlcmd = "FLUSH PRIVILEGES"
|
|
410
|
+
try:
|
|
411
|
+
result = self.__conn.execute(sqlcmd)
|
|
412
|
+
logger.info("flushed privileges")
|
|
413
|
+
except:
|
|
414
|
+
logger.exception("failed to flush privileges")
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
elif url.drivername.lower() == "postgresql":
|
|
418
|
+
if not self.exists_role(url.username):
|
|
419
|
+
logger.info("creating role %s", url.username)
|
|
420
|
+
if not self.create_role(url.username, url.password):
|
|
421
|
+
logger.error("failed to create role %s", url.username)
|
|
422
|
+
|
|
423
|
+
self.create_role(url.username, url.password, update=True)
|
|
424
|
+
|
|
425
|
+
sqlcmd = "GRANT ALL PRIVILEGES ON DATABASE {} TO {}".format(
|
|
426
|
+
self.db_name, url.username)
|
|
427
|
+
logger.info(sqlcmd)
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
result = self.__conn.execute(sqlcmd)
|
|
431
|
+
if result:
|
|
432
|
+
logger.debug("... OK")
|
|
433
|
+
except:
|
|
434
|
+
logger.error("failed to grant ALL to '%s' on db '%s'",
|
|
435
|
+
url.username, self.db_name)
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
def create_role(self, role_name, role_password, update=False,
|
|
441
|
+
role_options="NOCREATEDB NOCREATEROLE LOGIN"):
|
|
442
|
+
"""
|
|
443
|
+
create a role object (PostgreSQL secific)
|
|
444
|
+
"""
|
|
445
|
+
# TODO: is any other dialect using the "role" concept?
|
|
446
|
+
if self.dialect != "postgresql":
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
# TODO: do this only for SQLAlchemy
|
|
450
|
+
if not self.__conn:
|
|
451
|
+
raise osdbError("connection not available")
|
|
452
|
+
|
|
453
|
+
if update:
|
|
454
|
+
sqlcmd = "ALTER USER {} WITH PASSWORD '{}' {}".format(
|
|
455
|
+
role_name, role_password, role_options)
|
|
456
|
+
else:
|
|
457
|
+
sqlcmd = "CREATE ROLE {} WITH {} PASSWORD '{}'".format(
|
|
458
|
+
role_name, role_options, role_password)
|
|
459
|
+
logger.info(sqlcmd)
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
result = self.__conn.execute(sqlcmd)
|
|
463
|
+
if result:
|
|
464
|
+
logger.info("role '{}' with options '{}' created".
|
|
465
|
+
format(role_name, role_options))
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logger.exception(e)
|
|
468
|
+
logger.error("creation of new role '%s' with options '%s' failed",
|
|
469
|
+
role_name, role_options)
|
|
470
|
+
return False
|
|
471
|
+
return result
|
|
472
|
+
|
|
473
|
+
def delete(self, table, filter_keys=None):
|
|
474
|
+
"""
|
|
475
|
+
delete a table object from a database
|
|
476
|
+
"""
|
|
477
|
+
# TODO: do this only for SQLAlchemy
|
|
478
|
+
if not self.__conn:
|
|
479
|
+
raise osdbError("connection not available")
|
|
480
|
+
|
|
481
|
+
where_str = self.get_where(filter_keys)
|
|
482
|
+
statement = "DELETE FROM {}{}".format(table, where_str)
|
|
483
|
+
try:
|
|
484
|
+
self.__conn.execute(statement)
|
|
485
|
+
except sqlalchemy.exc.SQLAlchemyError as ex:
|
|
486
|
+
logger.error("cannot execute query: {}".format(statement))
|
|
487
|
+
logger.error(ex)
|
|
488
|
+
return False
|
|
489
|
+
return True
|
|
490
|
+
|
|
491
|
+
def destroy(self):
|
|
492
|
+
"""
|
|
493
|
+
decontructor of a database object
|
|
494
|
+
"""
|
|
495
|
+
# TODO: do this only for SQLAlchemy
|
|
496
|
+
if not self.__conn:
|
|
497
|
+
return
|
|
498
|
+
self.__conn.close()
|
|
499
|
+
|
|
500
|
+
def drop(self):
|
|
501
|
+
"""
|
|
502
|
+
drop a database object
|
|
503
|
+
"""
|
|
504
|
+
# TODO: do this only for SQLAlchemy
|
|
505
|
+
if not self.__conn:
|
|
506
|
+
raise osdbError("connection not available")
|
|
507
|
+
if self.dialect != "sqlite":
|
|
508
|
+
database_url = self.set_url_db(self.db_url, self.db_name)
|
|
509
|
+
else:
|
|
510
|
+
database_url = 'sqlite:///' + self.db_name
|
|
511
|
+
try:
|
|
512
|
+
sqlalchemy_utils.drop_database(database_url)
|
|
513
|
+
logger.debug("database '%s' dropped", self.db_name)
|
|
514
|
+
return True
|
|
515
|
+
except sqlalchemy.exc.NoSuchModuleError as me:
|
|
516
|
+
logger.error("cannot check if database {} exists: {}".
|
|
517
|
+
format(self.db_name, me))
|
|
518
|
+
raise osdbError("cannot handle {} dialect".
|
|
519
|
+
format(self.dialect)) from None
|
|
520
|
+
|
|
521
|
+
def drop_role(self, role_name):
|
|
522
|
+
"""
|
|
523
|
+
drop a role object (PostgreSQL specific)
|
|
524
|
+
"""
|
|
525
|
+
# TODO: is any other dialect using the "role" concept?
|
|
526
|
+
if self.dialect != "postgresql":
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
# TODO: do this only for SQLAlchemy
|
|
530
|
+
if not self.__conn:
|
|
531
|
+
raise osdbError("connection not available")
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
logger.debug("Role '%s' will be dropped", role_name)
|
|
535
|
+
|
|
536
|
+
sqlcmd = "DROP ROLE IF EXISTS {}".format(role_name)
|
|
537
|
+
try:
|
|
538
|
+
result = self.__conn.execute(sqlcmd)
|
|
539
|
+
if result:
|
|
540
|
+
logger.debug("Role '%s' dropped", role_name)
|
|
541
|
+
except:
|
|
542
|
+
logger.error("dropping role '%s' failed", role_name)
|
|
543
|
+
return False
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
def entry_exists(self, table, constraints):
|
|
547
|
+
"""
|
|
548
|
+
check for existence of table constraints
|
|
549
|
+
"""
|
|
550
|
+
ret = self.find(table, "count(*)", constraints)
|
|
551
|
+
if ret and ret.first()[0] != 0:
|
|
552
|
+
return True
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
def exec_sql_file(self, sql_file):
|
|
556
|
+
"""
|
|
557
|
+
deploy given sql file
|
|
558
|
+
"""
|
|
559
|
+
# TODO: do this only for SQLAlchemy
|
|
560
|
+
if not self.__conn:
|
|
561
|
+
raise osdbError("connection not available")
|
|
562
|
+
|
|
563
|
+
with open(sql_file, 'r') as f:
|
|
564
|
+
if sql_file.endswith("-migrate.sql"):
|
|
565
|
+
try:
|
|
566
|
+
sql = f.read()
|
|
567
|
+
|
|
568
|
+
# the DELIMITER thingies are only useful to mysql shell client
|
|
569
|
+
sql = re.sub(r'DELIMITER .*\n', '', sql)
|
|
570
|
+
sql = re.sub(r'\$\$', ';', sql)
|
|
571
|
+
|
|
572
|
+
# DROP/CREATE PROCEDURE statements seem to only work separately
|
|
573
|
+
sql = re.sub(r'DROP PROCEDURE .*\n', '', sql)
|
|
574
|
+
|
|
575
|
+
self.__conn.execute(sql)
|
|
576
|
+
except sqlalchemy.exc.IntegrityError as ie:
|
|
577
|
+
raise osdbError("cannot deploy {} file: {}".
|
|
578
|
+
format(sql_file, ie)) from None
|
|
579
|
+
else:
|
|
580
|
+
for sql in f.read().split(";"):
|
|
581
|
+
sql = sql.strip()
|
|
582
|
+
if not sql:
|
|
583
|
+
continue
|
|
584
|
+
try:
|
|
585
|
+
self.__conn.execute(sql)
|
|
586
|
+
except sqlalchemy.exc.IntegrityError as ie:
|
|
587
|
+
raise osdbModuleAlreadyExistsError(
|
|
588
|
+
"cannot deploy {} file: {}".format(sql_file, ie)) from None
|
|
589
|
+
|
|
590
|
+
def exists(self, db=None):
|
|
591
|
+
"""
|
|
592
|
+
check for existence of a database object
|
|
593
|
+
"""
|
|
594
|
+
check_db = db if db is not None else self.db_name
|
|
595
|
+
# TODO: do this only for SQLAlchemy
|
|
596
|
+
if not self.__conn:
|
|
597
|
+
return False
|
|
598
|
+
|
|
599
|
+
if self.dialect != "sqlite":
|
|
600
|
+
database_url = self.set_url_db(self.db_url, check_db)
|
|
601
|
+
else:
|
|
602
|
+
database_url = 'sqlite:///' + check_db
|
|
603
|
+
|
|
604
|
+
logger.debug("check database URL '{}'".format(database_url))
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
if sqlalchemy_utils.database_exists(database_url):
|
|
608
|
+
logger.debug("DB '{}' exists".format(check_db))
|
|
609
|
+
return True
|
|
610
|
+
except sqlalchemy.exc.NoSuchModuleError as me:
|
|
611
|
+
logger.error("cannot check if database {} exists: {}".
|
|
612
|
+
format(check_db, me))
|
|
613
|
+
raise osdbError("cannot handle {} dialect".
|
|
614
|
+
format(self.dialect)) from None
|
|
615
|
+
|
|
616
|
+
logger.debug("DB does not exist")
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
def exists_role(self, role_name=None):
|
|
620
|
+
"""
|
|
621
|
+
check for existence of a role object (PostgreSQL specific)
|
|
622
|
+
"""
|
|
623
|
+
# TODO: is any other dialect using the "role" concept?
|
|
624
|
+
if self.dialect != "postgresql":
|
|
625
|
+
return False
|
|
626
|
+
|
|
627
|
+
# TODO: do this only for SQLAlchemy
|
|
628
|
+
if not self.__conn:
|
|
629
|
+
raise osdbError("connection not available")
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
if role_name is None:
|
|
633
|
+
role_name = 'opensips'
|
|
634
|
+
|
|
635
|
+
filter_args = {'rolname': role_name}
|
|
636
|
+
logger.debug("filter argument: '%s'", filter_args)
|
|
637
|
+
|
|
638
|
+
role_count = self.__session.query(Roles).\
|
|
639
|
+
filter_by(**filter_args).\
|
|
640
|
+
count()
|
|
641
|
+
logger.debug("Number of matching role instances: '%s'", role_count)
|
|
642
|
+
|
|
643
|
+
if role_count >= 1:
|
|
644
|
+
logger.debug("Role instance '%s' exists", role_name)
|
|
645
|
+
return True
|
|
646
|
+
else:
|
|
647
|
+
logger.debug("Role instance '%s' does not exist", role_name)
|
|
648
|
+
return False
|
|
649
|
+
|
|
650
|
+
def find(self, table, fields, filter_keys):
|
|
651
|
+
"""
|
|
652
|
+
match fields in a given table
|
|
653
|
+
"""
|
|
654
|
+
# TODO: do this only for SQLAlchemy
|
|
655
|
+
if not self.__conn:
|
|
656
|
+
raise osdbError("connection not available")
|
|
657
|
+
if not fields:
|
|
658
|
+
fields = ['*']
|
|
659
|
+
elif type(fields) != list:
|
|
660
|
+
fields = [ fields ]
|
|
661
|
+
|
|
662
|
+
where_str = self.get_where(filter_keys)
|
|
663
|
+
statement = "SELECT {} FROM {}{}".format(
|
|
664
|
+
", ".join(fields),
|
|
665
|
+
table,
|
|
666
|
+
where_str)
|
|
667
|
+
try:
|
|
668
|
+
result = self.__conn.execute(statement)
|
|
669
|
+
except sqlalchemy.exc.SQLAlchemyError as ex:
|
|
670
|
+
logger.error("cannot execute query: {}".format(statement))
|
|
671
|
+
logger.error(ex)
|
|
672
|
+
return None
|
|
673
|
+
return result
|
|
674
|
+
|
|
675
|
+
def get_dialect(url):
|
|
676
|
+
"""
|
|
677
|
+
extract database dialect from an url
|
|
678
|
+
"""
|
|
679
|
+
return url.split('://')[0]
|
|
680
|
+
|
|
681
|
+
def get_where(self, filter_keys):
|
|
682
|
+
"""
|
|
683
|
+
construct a sql 'where clause' from given filter keys
|
|
684
|
+
"""
|
|
685
|
+
if filter_keys:
|
|
686
|
+
where_str = ""
|
|
687
|
+
for k, v in filter_keys.items():
|
|
688
|
+
where_str += " AND {} = ".format(k)
|
|
689
|
+
if type(v) == int:
|
|
690
|
+
where_str += str(v)
|
|
691
|
+
else:
|
|
692
|
+
where_str += "'{}'".format(
|
|
693
|
+
v.translate(str.maketrans({'\'': '\\\''})))
|
|
694
|
+
if where_str != "":
|
|
695
|
+
where_str = " WHERE " + where_str[5:]
|
|
696
|
+
else:
|
|
697
|
+
where_str = ""
|
|
698
|
+
return where_str
|
|
699
|
+
|
|
700
|
+
def get_role(self, role_name="opensips"):
|
|
701
|
+
"""
|
|
702
|
+
get attibutes of a role object (PostgreSQL specific)
|
|
703
|
+
"""
|
|
704
|
+
# TODO: is any other dialect using the "role" concept?
|
|
705
|
+
if self.dialect != "postgresql":
|
|
706
|
+
return False
|
|
707
|
+
|
|
708
|
+
# TODO: do this only for SQLAlchemy
|
|
709
|
+
if not self.__conn:
|
|
710
|
+
raise osdbError("connection not available")
|
|
711
|
+
return False
|
|
712
|
+
|
|
713
|
+
# query elements for the given role
|
|
714
|
+
role_element = self.__session.query(Roles).\
|
|
715
|
+
filter(Roles.rolname == role_name).all()
|
|
716
|
+
|
|
717
|
+
# create a dictionary and output the key-value pairs
|
|
718
|
+
for row in role_element:
|
|
719
|
+
#print ("role: ", row.rolname, "(password:", row.rolpassword, "canlogin:", row.rolcanlogin, ")")
|
|
720
|
+
dict = self.row2dict(row)
|
|
721
|
+
for key in sorted(dict, key=lambda k: dict[k], reverse=True):
|
|
722
|
+
print (key + ": " + dict[key])
|
|
723
|
+
logger.debug("role_elements: %s", dict)
|
|
724
|
+
|
|
725
|
+
def grant_db_options(self, role_name, on_statement, privs="ALL PRIVILEGES"):
|
|
726
|
+
"""
|
|
727
|
+
assign attibutes to a role object (PostgreSQL specific)
|
|
728
|
+
"""
|
|
729
|
+
# TODO: is any other dialect using the "role" concept?
|
|
730
|
+
if self.dialect != "postgresql":
|
|
731
|
+
return False
|
|
732
|
+
|
|
733
|
+
# TODO: do this only for SQLAlchemy
|
|
734
|
+
if not self.__conn:
|
|
735
|
+
raise osdbError("connection not available")
|
|
736
|
+
|
|
737
|
+
sqlcmd = "GRANT {} {} TO {}".format(privs, on_statement, role_name)
|
|
738
|
+
logger.info(sqlcmd)
|
|
739
|
+
|
|
740
|
+
try:
|
|
741
|
+
self.__conn.execute(sqlcmd)
|
|
742
|
+
except Exception as e:
|
|
743
|
+
logger.exception(e)
|
|
744
|
+
logger.error("failed to grant '%s' '%s' to '%s'", privs, on_statement, role_name)
|
|
745
|
+
return False
|
|
746
|
+
|
|
747
|
+
return True
|
|
748
|
+
|
|
749
|
+
def grant_public_schema(self, role_name):
|
|
750
|
+
self.grant_db_options(role_name, "ON SCHEMA public")
|
|
751
|
+
|
|
752
|
+
def grant_table_options(self, role, table, privs="ALL PRIVILEGES"):
|
|
753
|
+
self.grant_db_options(role, "ON TABLE {}".format(table))
|
|
754
|
+
|
|
755
|
+
def has_sqlalchemy():
|
|
756
|
+
"""
|
|
757
|
+
check for usability of the SQLAlchemy modules
|
|
758
|
+
"""
|
|
759
|
+
return sqlalchemy_available
|
|
760
|
+
|
|
761
|
+
def has_dialect(dialect):
|
|
762
|
+
"""
|
|
763
|
+
check for support of a given database dialect via SQLAlchemy
|
|
764
|
+
"""
|
|
765
|
+
# TODO: do this only for SQLAlchemy
|
|
766
|
+
try:
|
|
767
|
+
sqlalchemy.create_engine('{}://'.format(dialect))
|
|
768
|
+
except sqlalchemy.exc.NoSuchModuleError:
|
|
769
|
+
return False
|
|
770
|
+
return True
|
|
771
|
+
|
|
772
|
+
def insert(self, table, keys):
|
|
773
|
+
"""
|
|
774
|
+
insert values into table
|
|
775
|
+
"""
|
|
776
|
+
# TODO: do this only for SQLAlchemy
|
|
777
|
+
if not self.__conn:
|
|
778
|
+
raise osdbError("connection not available")
|
|
779
|
+
|
|
780
|
+
values = ""
|
|
781
|
+
for v in keys.values():
|
|
782
|
+
values += ", "
|
|
783
|
+
if type(v) == int:
|
|
784
|
+
values += v
|
|
785
|
+
else:
|
|
786
|
+
values += "'{}'".format(
|
|
787
|
+
v.translate(str.maketrans({'\'': '\\\''})))
|
|
788
|
+
statement = "INSERT INTO {} ({}) VALUES ({})".format(
|
|
789
|
+
table, ", ".join(keys.keys()), values[2:])
|
|
790
|
+
try:
|
|
791
|
+
result = self.__conn.execute(statement)
|
|
792
|
+
except sqlalchemy.exc.SQLAlchemyError as ex:
|
|
793
|
+
logger.error("cannot execute query: {}".format(statement))
|
|
794
|
+
logger.error(ex)
|
|
795
|
+
return False
|
|
796
|
+
return result
|
|
797
|
+
|
|
798
|
+
def migrate(self, proc_suffix, migrate_scripts, old_db, new_db, tables=[]):
|
|
799
|
+
"""
|
|
800
|
+
migrate from source to destination database using SQL schema files
|
|
801
|
+
@flavour: values should resemble: '2.4_to_3.0', '3.0_to_3.1'
|
|
802
|
+
@sp_suffix: stored procedure name suffix, specific to each migration
|
|
803
|
+
"""
|
|
804
|
+
|
|
805
|
+
if self.dialect != "mysql":
|
|
806
|
+
logger.error("Table data migration is only supported for MySQL!")
|
|
807
|
+
return
|
|
808
|
+
|
|
809
|
+
proc_db_migrate = 'OSIPS_DB_MIGRATE_{}'.format(proc_suffix)
|
|
810
|
+
proc_tb_migrate = 'OSIPS_TB_COPY_{}'.format(proc_suffix)
|
|
811
|
+
|
|
812
|
+
self.connect(old_db)
|
|
813
|
+
|
|
814
|
+
# separately drop DB/table migration stored procedures if already
|
|
815
|
+
# present, since there are issues with multiple statements in 1 import
|
|
816
|
+
try:
|
|
817
|
+
self.__conn.execute(sqlalchemy.sql.text(
|
|
818
|
+
"DROP PROCEDURE IF EXISTS {}".format(proc_db_migrate)).
|
|
819
|
+
execution_options(autocommit=True))
|
|
820
|
+
|
|
821
|
+
self.__conn.execute(sqlalchemy.sql.text(
|
|
822
|
+
"DROP PROCEDURE IF EXISTS {}".format(proc_tb_migrate)).
|
|
823
|
+
execution_options(autocommit=True))
|
|
824
|
+
except:
|
|
825
|
+
logger.exception("Failed to drop migration stored procedures!")
|
|
826
|
+
|
|
827
|
+
for ms in migrate_scripts:
|
|
828
|
+
logger.debug("Importing {}...".format(ms))
|
|
829
|
+
self.exec_sql_file(ms)
|
|
830
|
+
|
|
831
|
+
if tables:
|
|
832
|
+
for tb in tables:
|
|
833
|
+
logger.info("Migrating {} data... ".format(tb))
|
|
834
|
+
try:
|
|
835
|
+
self.__conn.execute(sqlalchemy.sql.text(
|
|
836
|
+
"CALL {}.{}('{}', '{}', '{}')".format(
|
|
837
|
+
old_db, proc_tb_migrate, old_db, new_db, tb)))
|
|
838
|
+
except Exception as e:
|
|
839
|
+
logger.exception(e)
|
|
840
|
+
logger.error("Failed to migrate '{}' table data, ".format(tb) +
|
|
841
|
+
"see above errors!")
|
|
842
|
+
else:
|
|
843
|
+
try:
|
|
844
|
+
self.__conn.execute(sqlalchemy.sql.text(
|
|
845
|
+
"CALL {}.{}('{}', '{}')".format(
|
|
846
|
+
old_db, proc_db_migrate, old_db, new_db)))
|
|
847
|
+
except Exception as e:
|
|
848
|
+
logger.exception(e)
|
|
849
|
+
logger.error("Failed to migrate database!")
|
|
850
|
+
|
|
851
|
+
print("Finished copying OpenSIPS table data " +
|
|
852
|
+
"into database '{}'!".format(new_db))
|
|
853
|
+
|
|
854
|
+
def row2dict(self, row):
|
|
855
|
+
"""
|
|
856
|
+
convert SQL table row to python dict
|
|
857
|
+
"""
|
|
858
|
+
dict = {}
|
|
859
|
+
for column in row.__table__.columns:
|
|
860
|
+
dict[column.name] = str(getattr(row, column.name))
|
|
861
|
+
|
|
862
|
+
return dict
|
|
863
|
+
|
|
864
|
+
def update(self, table, update_keys, filter_keys=None):
|
|
865
|
+
"""
|
|
866
|
+
update table
|
|
867
|
+
"""
|
|
868
|
+
# TODO: do this only for SQLAlchemy
|
|
869
|
+
if not self.__conn:
|
|
870
|
+
raise osdbError("connection not available")
|
|
871
|
+
|
|
872
|
+
update_str = ""
|
|
873
|
+
for k, v in update_keys.items():
|
|
874
|
+
update_str += ", {} = ".format(k)
|
|
875
|
+
if type(v) == int:
|
|
876
|
+
update_str += v
|
|
877
|
+
else:
|
|
878
|
+
update_str += "'{}'".format(
|
|
879
|
+
v.translate(str.maketrans({'\'': '\\\''})))
|
|
880
|
+
where_str = self.get_where(filter_keys)
|
|
881
|
+
statement = "UPDATE {} SET {}{}".format(table,
|
|
882
|
+
update_str[2:], where_str)
|
|
883
|
+
try:
|
|
884
|
+
result = self.__conn.execute(statement)
|
|
885
|
+
except sqlalchemy.exc.SQLAlchemyError as ex:
|
|
886
|
+
logger.error("cannot execute query: {}".format(statement))
|
|
887
|
+
logger.error(ex)
|
|
888
|
+
return False
|
|
889
|
+
return result
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@staticmethod
|
|
893
|
+
def get_db_engine():
|
|
894
|
+
if cfg.exists('database_admin_url'):
|
|
895
|
+
engine = osdb.get_url_driver(cfg.get('database_admin_url'))
|
|
896
|
+
elif cfg.exists('database_url'):
|
|
897
|
+
engine = osdb.get_url_driver(cfg.get('database_url'))
|
|
898
|
+
else:
|
|
899
|
+
engine = "mysql"
|
|
900
|
+
|
|
901
|
+
if engine not in SUPPORTED_BACKENDS:
|
|
902
|
+
logger.error("bad database engine ({}), supported: {}".format(
|
|
903
|
+
engine, " ".join(SUPPORTED_BACKENDS)))
|
|
904
|
+
return None
|
|
905
|
+
return engine
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@staticmethod
|
|
909
|
+
def get_db_host():
|
|
910
|
+
if cfg.exists('database_admin_url'):
|
|
911
|
+
return osdb.get_url_host(cfg.get('database_admin_url'))
|
|
912
|
+
elif cfg.exists('database_url'):
|
|
913
|
+
return osdb.get_url_host(cfg.get('database_url'))
|
|
914
|
+
|
|
915
|
+
return "localhost"
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
@staticmethod
|
|
919
|
+
def set_url_db(url, db):
|
|
920
|
+
"""Force a given database @url string to include the given @db.
|
|
921
|
+
|
|
922
|
+
Args:
|
|
923
|
+
url (str): the URL to change the DB for.
|
|
924
|
+
db (str): the name of the database to set. If None, the database
|
|
925
|
+
part will be removed from the URL.
|
|
926
|
+
"""
|
|
927
|
+
at_idx = url.find('@')
|
|
928
|
+
if at_idx < 0:
|
|
929
|
+
logger.error("Bad database URL: {}, missing host part".format(url))
|
|
930
|
+
return None
|
|
931
|
+
|
|
932
|
+
db_idx = url.find('/', at_idx)
|
|
933
|
+
if db_idx < 0:
|
|
934
|
+
if db is None:
|
|
935
|
+
return url
|
|
936
|
+
return url + '/' + db
|
|
937
|
+
else:
|
|
938
|
+
if db is None:
|
|
939
|
+
return url[:db_idx]
|
|
940
|
+
return url[:db_idx+1] + db
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
@staticmethod
|
|
944
|
+
def set_url_driver(url, driver):
|
|
945
|
+
return driver + url[url.find(':'):]
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
@staticmethod
|
|
949
|
+
def set_url_password(url, password):
|
|
950
|
+
url = make_url(url)
|
|
951
|
+
url.password = DBURL.escape_pass(password)
|
|
952
|
+
return str(url)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
@staticmethod
|
|
956
|
+
def set_url_host(url, host):
|
|
957
|
+
url = make_url(url)
|
|
958
|
+
url.host = host
|
|
959
|
+
return str(url)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
@staticmethod
|
|
963
|
+
def get_url_driver(url, capitalize=False):
|
|
964
|
+
if capitalize:
|
|
965
|
+
driver = make_url(url).drivername.lower()
|
|
966
|
+
capitalized = {
|
|
967
|
+
'mysql': 'MySQL',
|
|
968
|
+
'postgresql': 'PostgreSQL',
|
|
969
|
+
'sqlite': 'SQLite',
|
|
970
|
+
'oracle': 'Oracle',
|
|
971
|
+
}
|
|
972
|
+
return capitalized.get(driver, driver.capitalize())
|
|
973
|
+
else:
|
|
974
|
+
return make_url(url).drivername.lower()
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
@staticmethod
|
|
978
|
+
def get_url_user(url):
|
|
979
|
+
return make_url(url).username
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
@staticmethod
|
|
983
|
+
def get_url_pswd(url):
|
|
984
|
+
return make_url(url).password
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
@staticmethod
|
|
988
|
+
def get_url_host(url):
|
|
989
|
+
return make_url(url).host
|