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/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