Skip to content

Commit 1612b54

Browse files
authored
Merge pull request #208 from timvaillancourt/1.2.0-ssl-pymongo
1.2.0: MongoDB SSL support (#60)
2 parents ea53998 + 26939b8 commit 1612b54

File tree

6 files changed

+123
-25
lines changed

6 files changed

+123
-25
lines changed

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Features
3535
- `Nagios NSCA <https://sourceforge.net/p/nagios/nsca>`__ push
3636
notification support (*optional*)
3737
- Modular backup, archiving, upload and notification components
38+
- Support for MongoDB Authentication and SSL database connections
3839
- Multi-threaded, single executable
3940
- Auto-scales to number of available CPUs by default
4041

@@ -221,6 +222,7 @@ Roadmap
221222
- Upload compatibility for ZBackup archive phase *(upload unsupported today)*
222223
- Backup retention/rotation *(eg: delete old backups)*
223224
- Support more notification methods *(Prometheus, PagerDuty, etc)*
225+
- Support more upload methods *(Rsync, etc)*
224226
- Support SSL MongoDB connections
225227
- Documentation for running under Docker with persistent volumes
226228
- Python unit tests

conf/mongodb-consistent-backup.example.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ production:
44
#username: [auth username] (default: none)
55
#password: [auth password] (default: none)
66
#authdb: [auth database] (default: admin)
7+
#ssl:
8+
# enabled: [true|false] (default: false)
9+
# insecure: [true|false] (default: false)
10+
# ca_file: [path]
11+
# crl_file: [path]
12+
# client_cert_file: [path]
713
log_dir: /var/log/mongodb-consistent-backup
814
backup:
915
method: mongodump

mongodb_consistent_backup/Backup/Mongodump/MongodumpThread.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from signal import signal, SIGINT, SIGTERM, SIG_IGN
99
from subprocess import Popen, PIPE
1010

11-
from mongodb_consistent_backup.Common import is_datetime
11+
from mongodb_consistent_backup.Common import is_datetime, parse_config_bool
1212
from mongodb_consistent_backup.Oplog import Oplog
1313

1414

@@ -25,10 +25,13 @@ def __init__(self, state, uri, timer, config, base_dir, version, threads=0, dump
2525
self.threads = threads
2626
self.dump_gzip = dump_gzip
2727

28-
self.user = self.config.username
29-
self.password = self.config.password
30-
self.authdb = self.config.authdb
31-
self.binary = self.config.backup.mongodump.binary
28+
self.user = self.config.username
29+
self.password = self.config.password
30+
self.authdb = self.config.authdb
31+
self.ssl_ca_file = self.config.ssl.ca_file
32+
self.ssl_crl_file = self.config.ssl.crl_file
33+
self.ssl_client_cert_file = self.config.ssl.client_cert_file
34+
self.binary = self.config.backup.mongodump.binary
3235

3336
self.timer_name = "%s-%s" % (self.__class__.__name__, self.uri.replset)
3437
self.exit_code = 1
@@ -52,6 +55,18 @@ def close(self, exit_code=None, frame=None):
5255
self._command.close()
5356
sys.exit(self.exit_code)
5457

58+
def do_ssl(self):
59+
return parse_config_bool(self.config.ssl.enabled)
60+
61+
def do_ssl_insecure(self):
62+
return parse_config_bool(self.config.ssl.insecure)
63+
64+
def is_version_gte(self, compare):
65+
if os.path.isfile(self.binary) and os.access(self.binary, os.X_OK):
66+
if tuple(compare.split(".")) <= tuple(self.version.split(".")):
67+
return True
68+
return False
69+
5570
def parse_mongodump_line(self, line):
5671
try:
5772
line = line.rstrip()
@@ -118,21 +133,40 @@ def mongodump_cmd(self):
118133
mongodump_flags = ["--host", mongodump_uri.host, "--port", str(mongodump_uri.port), "--oplog", "--out", "%s/dump" % self.backup_dir]
119134
if self.threads > 0:
120135
mongodump_flags.extend(["--numParallelCollections=" + str(self.threads)])
136+
121137
if self.dump_gzip:
122138
mongodump_flags.extend(["--gzip"])
123-
if tuple("3.4.0".split(".")) <= tuple(self.version.split(".")):
139+
140+
if self.is_version_gte("3.4.0"):
124141
mongodump_flags.extend(["--readPreference=secondary"])
142+
125143
if self.authdb and self.authdb != "admin":
126144
logging.debug("Using database %s for authentication" % self.authdb)
127145
mongodump_flags.extend(["--authenticationDatabase", self.authdb])
128146
if self.user and self.password:
129147
# >= 3.0.2 supports password input via stdin to mask from ps
130-
if tuple(self.version.split(".")) >= tuple("3.0.2".split(".")):
148+
if self.is_version_gte("3.0.2"):
131149
mongodump_flags.extend(["-u", self.user, "-p", '""'])
132150
self.do_stdin_passwd = True
133151
else:
134152
logging.warning("Mongodump is too old to set password securely! Upgrade to mongodump >= 3.0.2 to resolve this")
135153
mongodump_flags.extend(["-u", self.user, "-p", self.password])
154+
155+
if self.do_ssl():
156+
if self.is_version_gte("2.6.0"):
157+
mongodump_flags.append("--ssl")
158+
if self.ssl_ca_file:
159+
mongodump_flags.extend(["--sslCAFile", self.ssl_ca_file])
160+
if self.ssl_crl_file:
161+
mongodump_flags.extend(["--sslCRLFile", self.ssl_crl_file])
162+
if self.client_cert_file:
163+
mongodump_flags.extend(["--sslPEMKeyFile", self.ssl_cert_file])
164+
if self.do_ssl_insecure():
165+
mongodump_flags.extend(["--sslAllowInvalidCertificates", "--sslAllowInvalidHostnames"])
166+
else:
167+
logging.fatal("Mongodump must be >= 2.6.0 to enable SSL encryption!")
168+
sys.exit(1)
169+
136170
mongodump_cmd.extend(mongodump_flags)
137171
return mongodump_cmd
138172

mongodb_consistent_backup/Common/Config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
88
from yconf.util import NestedDict
99

1010

11+
def parse_config_bool(item):
12+
try:
13+
if isinstance(item, bool):
14+
return item
15+
elif isinstance(item, str):
16+
if item.rstrip().lower() is "true":
17+
return True
18+
return False
19+
except:
20+
return False
21+
22+
1123
class PrintVersions(Action):
1224
def __init__(self, option_strings, dest, nargs=0, **kwargs):
1325
super(PrintVersions, self).__init__(option_strings=option_strings, dest=dest, nargs=nargs, **kwargs)
@@ -54,6 +66,11 @@ def makeParser(self):
5466
parser.add_argument("-u", "--user", "--username", dest="username", help="MongoDB Authentication Username (for optional auth)", type=str)
5567
parser.add_argument("-p", "--password", dest="password", help="MongoDB Authentication Password (for optional auth)", type=str)
5668
parser.add_argument("-a", "--authdb", dest="authdb", help="MongoDB Auth Database (for optional auth - default: admin)", default='admin', type=str)
69+
parser.add_argument("--ssl.enabled", dest="ssl.enabled", help="Use SSL secured database connections to MongoDB hosts (default: false)", default=False, action="store_true")
70+
parser.add_argument("--ssl.insecure", dest="ssl.insecure", help="Do not validate the SSL certificate and hostname of the server (default: false)", default=False, action="store_true")
71+
parser.add_argument("--ssl.ca_file", dest="ssl.ca_file", help="Path to SSL Certificate Authority file in PEM format (default: use OS default CA)", default=None, type=str)
72+
parser.add_argument("--ssl.crl_file", dest="ssl.crl_file", help="Path to SSL Certificate Revocation List file in PEM or DER format (for optional cert revocation)", default=None, type=str)
73+
parser.add_argument("--ssl.client_cert_file", dest="ssl.client_cert_file", help="Path to Client SSL Certificate file in PEM format (for optional client ssl auth)", default=None, type=str)
5774
parser.add_argument("-L", "--log-dir", dest="log_dir", help="Path to write log files to (default: disabled)", default='', type=str)
5875
parser.add_argument("--lock-file", dest="lock_file", help="Location of lock file (default: /tmp/mongodb-consistent-backup.lock)", default='/tmp/mongodb-consistent-backup.lock', type=str)
5976
parser.add_argument("--sharding.balancer.wait_secs", dest="sharding.balancer.wait_secs", help="Maximum time to wait for balancer to stop, in seconds (default: 300)", default=300, type=int)

mongodb_consistent_backup/Common/DB.py

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,85 @@
44
from inspect import currentframe, getframeinfo
55
from pymongo import DESCENDING, CursorType, MongoClient
66
from pymongo.errors import ConnectionFailure, OperationFailure, ServerSelectionTimeoutError
7+
from ssl import CERT_REQUIRED, CERT_NONE
78
from time import sleep
89

10+
from mongodb_consistent_backup.Common import parse_config_bool
911
from mongodb_consistent_backup.Errors import DBAuthenticationError, DBConnectionError, DBOperationError, Error
1012

1113

1214
class DB:
1315
def __init__(self, uri, config, do_replset=False, read_pref='primaryPreferred', do_connect=True, conn_timeout=5000, retries=5):
1416
self.uri = uri
15-
self.username = config.username
16-
self.password = config.password
17-
self.authdb = config.authdb
17+
self.config = config
1818
self.do_replset = do_replset
1919
self.read_pref = read_pref
2020
self.do_connect = do_connect
2121
self.conn_timeout = conn_timeout
2222
self.retries = retries
2323

24+
self.username = self.config.username
25+
self.password = self.config.password
26+
self.authdb = self.config.authdb
27+
self.ssl_ca_file = self.config.ssl.ca_file
28+
self.ssl_crl_file = self.config.ssl.crl_file
29+
self.ssl_client_cert_file = self.config.ssl.client_cert_file
30+
2431
self.replset = None
2532
self._conn = None
2633
self._is_master = None
34+
2735
self.connect()
2836
self.auth_if_required()
2937

38+
def do_ssl(self):
39+
return parse_config_bool(self.config.ssl.enabled)
40+
41+
def do_ssl_insecure(self):
42+
return parse_config_bool(self.config.ssl.insecure)
43+
44+
def client_opts(self):
45+
opts = {
46+
"connect": self.do_connect,
47+
"host": self.uri.hosts(),
48+
"connectTimeoutMS": self.conn_timeout,
49+
"serverSelectionTimeoutMS": self.conn_timeout,
50+
"maxPoolSize": 1,
51+
}
52+
if self.do_replset:
53+
self.replset = self.uri.replset
54+
opts.update({
55+
"replicaSet": self.replset,
56+
"readPreference": self.read_pref,
57+
"w": "majority"
58+
})
59+
if self.do_ssl():
60+
logging.debug("Using SSL-secured mongodb connection (ca_cert=%s, client_cert=%s, crl_file=%s, insecure=%s)" % (
61+
self.ssl_ca_file,
62+
self.ssl_client_cert_file,
63+
self.ssl_crl_file,
64+
self.do_ssl_insecure()
65+
))
66+
opts.update({
67+
"ssl": True,
68+
"ssl_ca_certs": self.ssl_ca_file,
69+
"ssl_crlfile": self.ssl_crl_file,
70+
"ssl_certfile": self.ssl_client_cert_file,
71+
"ssl_cert_reqs": CERT_REQUIRED,
72+
})
73+
if self.do_ssl_insecure():
74+
opts["ssl_cert_reqs"] = CERT_NONE
75+
return opts
76+
3077
def connect(self):
3178
try:
32-
if self.do_replset:
33-
self.replset = self.uri.replset
34-
logging.debug("Getting MongoDB connection to %s (replicaSet=%s, readPreference=%s)" % (
35-
self.uri, self.replset, self.read_pref
79+
logging.debug("Getting MongoDB connection to %s (replicaSet=%s, readPreference=%s, ssl=%s)" % (
80+
self.uri,
81+
self.replset,
82+
self.read_pref,
83+
self.do_ssl(),
3684
))
37-
conn = MongoClient(
38-
connect=self.do_connect,
39-
host=self.uri.hosts(),
40-
replicaSet=self.replset,
41-
readPreference=self.read_pref,
42-
connectTimeoutMS=self.conn_timeout,
43-
serverSelectionTimeoutMS=self.conn_timeout,
44-
maxPoolSize=1,
45-
w="majority"
46-
)
85+
conn = MongoClient(**self.client_opts())
4786
if self.do_connect:
4887
conn['admin'].command({"ping": 1})
4988
except (ConnectionFailure, OperationFailure, ServerSelectionTimeoutError), e:

mongodb_consistent_backup/Common/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from Config import Config # NOQA
1+
from Config import Config, parse_config_bool # NOQA
22
from DB import DB # NOQA
33
from LocalCommand import LocalCommand # NOQA
44
from Lock import Lock # NOQA

0 commit comments

Comments
 (0)