Skip to content

Commit 92563be

Browse files
committed
WL#10081: DevAPI IPv6 Support
This feature adds IPv6 support to Connector/Python. Currently, the user can connect to a MySQL server running the XPlugin using a hostname that resolves to an IPv4 address. With this feature, the hostname maybe resolved as IPv4 or IPv6 address. This patch also updates the address list parsing from URI string to correctly parse IPv6 addresses.
1 parent 14e6ee7 commit 92563be

File tree

5 files changed

+157
-69
lines changed

5 files changed

+157
-69
lines changed

lib/mysqlx/__init__.py

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# MySQL Connector/Python - MySQL driver written in Python.
2-
# Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
2+
# Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
33

44
# MySQL Connector/Python is licensed under the terms of the GPLv2
55
# <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
@@ -24,7 +24,6 @@
2424
"""MySQL X DevAPI Python implementation"""
2525

2626
import re
27-
2827
from . import constants
2928

3029
from .compat import STRING_TYPES, urlparse, unquote, parse_qsl
@@ -45,49 +44,42 @@
4544
AlterViewStatement, ColumnDef,
4645
GeneratedColumnDef, ForeignKeyDef, Expr)
4746

47+
_SPLIT = re.compile(r',(?![^\(\)]*\))')
48+
_PRIORITY = re.compile(r'^\(address=(.+),priority=(\d+)\)$', re.VERBOSE)
4849

49-
def _parse_address_list(address_list):
50+
def _parse_address_list(path):
5051
"""Parses a list of host, port pairs
5152
5253
Args:
53-
address_list: String containing a list of routers or just router
54+
path: String containing a list of routers or just router
5455
5556
Returns:
5657
Returns a dict with parsed values of host, port and priority if
5758
specified.
5859
"""
59-
is_list = re.compile(r'^\[(?![^,]*\]).*]$')
60-
hst_list = re.compile(r',(?![^\(\)]*\))')
61-
pri_addr = re.compile(r'^\(address\s*=\s*(?P<address>.+)\s*,\s*priority\s*=\s*(?P<priority>\d+)\)$')
60+
path = path.replace(" ", "")
61+
array = not("," not in path and path.count(":") > 1
62+
and path.count("[") == 1) and \
63+
path.startswith("[") and path.endswith("]")
6264

6365
routers = []
64-
if is_list.match(address_list):
65-
address_list = address_list.strip("[]")
66-
address_list = hst_list.split(address_list)
67-
else:
68-
match = urlparse("//{0}".format(address_list))
69-
return {
70-
"host": match.hostname,
71-
"port": match.port
72-
}
73-
74-
while address_list:
66+
address_list = _SPLIT.split(path[1:-1] if array else path)
67+
for address in address_list:
7568
router = {}
76-
address = address_list.pop(0).strip()
77-
match = pri_addr.match(address)
69+
70+
match = _PRIORITY.match(address)
7871
if match:
79-
address = match.group("address").strip()
80-
router["priority"] = int(match.group("priority"))
72+
address = match.group(1)
73+
router["priority"] = int(match.group(2))
8174

8275
match = urlparse("//{0}".format(address))
8376
if not match.hostname:
8477
raise InterfaceError("Invalid address: {0}".format(address))
8578

86-
router["host"] = match.hostname
87-
router["port"] = match.port
79+
router.update(host=match.hostname, port=match.port)
8880
routers.append(router)
8981

90-
return { "routers": routers }
82+
return {"routers": routers} if array else routers[0]
9183

9284
def _parse_connection_uri(uri):
9385
"""Parses the connection string and returns a dictionary with the

lib/mysqlx/connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ def __init__(self):
5252
self._is_ssl = False
5353

5454
def connect(self, params):
55-
s_type = socket.AF_INET if isinstance(params, tuple) else socket.AF_UNIX
55+
if isinstance(params, tuple):
56+
s_type = socket.AF_INET6 if ":" in params[0] else socket.AF_INET
57+
else:
58+
s_type = socket.AF_UNIX
5659
self._socket = socket.socket(s_type, socket.SOCK_STREAM)
5760
self._socket.connect(params)
5861

tests/mysqld.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# MySQL Connector/Python - MySQL driver written in Python.
2-
# Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved.
2+
# Copyright (c) 2009, 2017, Oracle and/or its affiliates. All rights reserved.
33

44
# MySQL Connector/Python is licensed under the terms of the GPLv2
55
# <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
@@ -344,8 +344,9 @@ class MySQLServer(MySQLServerBase):
344344
"""Class for managing a MySQL server"""
345345

346346
def __init__(self, basedir, topdir, cnf, bind_address, port, mysqlx_port,
347-
name, datadir=None, tmpdir=None,
347+
name, datadir=None, tmpdir=None, extra_args={},
348348
unix_socket_folder=None, ssl_folder=None, sharedir=None):
349+
self._extra_args = extra_args
349350
self._cnf = cnf
350351
self._option_file = os.path.join(topdir, 'my.cnf')
351352
self._bind_address = bind_address
@@ -458,6 +459,11 @@ def bootstrap(self):
458459
"'Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y',"
459460
"'Y','Y','Y','Y','Y','','','','',0,0,0,0,"
460461
"@@default_authentication_plugin,'','N',"
462+
"CURRENT_TIMESTAMP,NULL{1}), ('::1','root'{0},"
463+
"'Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y',"
464+
"'Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y',"
465+
"'Y','Y','Y','Y','Y','','','','',0,0,0,0,"
466+
"@@default_authentication_plugin,'','N',"
461467
"CURRENT_TIMESTAMP,NULL{1});"
462468
)
463469
# MySQL 5.7.5+ creates no user while bootstrapping
@@ -573,6 +579,13 @@ def start(self):
573579
'lc_messages_dir': _convert_forward_slash(
574580
self._lc_messages_dir),
575581
}
582+
583+
for arg in self._extra_args:
584+
if self._version < arg["version"]:
585+
options.update(dict([(key, '') for key in
586+
arg["options"].keys()]))
587+
else:
588+
options.update(arg["options"])
576589
try:
577590
fp = open(self._option_file, 'w')
578591
fp.write(self._cnf.format(**options))

tests/test_mysqlx_connection.py

Lines changed: 114 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
# MySQL Connector/Python - MySQL driver written in Python.
3-
# Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
3+
# Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
44

55
# MySQL Connector/Python is licensed under the terms of the GPLv2
66
# <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most
@@ -32,9 +32,9 @@
3232
import mysqlx
3333

3434
if mysqlx.compat.PY3:
35-
from urllib.parse import quote
35+
from urllib.parse import quote_plus
3636
else:
37-
from urllib import quote
37+
from urllib import quote_plus
3838

3939
LOGGER = logging.getLogger(tests.LOGGER_NAME)
4040

@@ -72,6 +72,26 @@
7272
("unicode:áé'í'óú@127.0.0.1",
7373
{"schema": "", "host": "127.0.0.1", "password": "áé'í'óú",
7474
"port": 33060, "user": "unicode"}),
75+
("root:@[localhost, 127.0.0.1:88, [::]:99, [a1:b1::]]",
76+
{"routers": [{"host": "localhost", "port": 33060},
77+
{"host": "127.0.0.1", "port": 88},
78+
{"host": "::", "port": 99},
79+
{"host": "a1:b1::", "port": 33060}],
80+
"user": "root", "password": "", "schema": ""}),
81+
("root:@[a1:a2:a3:a4:a5:a6:a7:a8]]",
82+
{"host": "a1:a2:a3:a4:a5:a6:a7:a8", "schema": "",
83+
"port": 33060, "user": "root", "password": ""}),
84+
("root:@localhost", {"user": "root", "password": "",
85+
"host": "localhost", "port": 33060, "schema": ""}),
86+
("root:@[a1:b1::]", {"user": "root", "password": "",
87+
"host": "a1:b1::", "port": 33060, "schema": ""}),
88+
("root:@[a1:b1::]:88", {"user": "root", "password": "",
89+
"host": "a1:b1::", "port": 88, "schema": ""}),
90+
("root:@[[a1:b1::]:88]", {"user": "root", "password": "",
91+
"routers": [{"host": "a1:b1::", "port":88}], "schema": ""}),
92+
("root:@[(address=localhost:99, priority=99)]",
93+
{"user": "root", "password": "", "schema": "",
94+
"routers": [{"host": "localhost", "port": 99, "priority": 99}]})
7595
)
7696

7797

@@ -89,6 +109,47 @@
89109
"priority": 98}], "password": "password", "user": "user"}),
90110
)
91111

112+
def build_uri(**kwargs):
113+
uri = "mysqlx://{0}:{1}".format(kwargs["user"], kwargs["password"])
114+
115+
if "host" in kwargs:
116+
host = "[{0}]".format(kwargs["host"]) \
117+
if ":" in kwargs["host"] else kwargs["host"]
118+
uri = "{0}@{1}".format(uri, host)
119+
elif "routers" in kwargs:
120+
routers = []
121+
for router in kwargs["routers"]:
122+
fmt = "(address={host}{port}, priority={priority})" \
123+
if "priority" in router else "{host}{port}"
124+
host = "[{0}]".format(router["host"]) if ":" in router["host"] \
125+
else router["host"]
126+
port = ":{0}".format(router["port"]) if "port" in router else ""
127+
128+
routers.append(fmt.format(host=host, port=port,
129+
priority=router.get("priority", None)))
130+
131+
uri = "{0}@[{1}]".format(uri, ",".join(routers))
132+
else:
133+
raise mysqlx.errors.ProgrammingError("host or routers required.")
134+
135+
if "port" in kwargs:
136+
uri = "{0}:{1}".format(uri, kwargs["port"])
137+
if "schema" in kwargs:
138+
uri = "{0}/{1}".format(uri, kwargs["schema"])
139+
140+
query = []
141+
if "ssl_ca" in kwargs:
142+
query.append("ssl-ca={0}".format(kwargs["ssl_ca"]))
143+
if "ssl_cert" in kwargs:
144+
query.append("ssl-cert={0}".format(kwargs["ssl_cert"]))
145+
if "ssl_key" in kwargs:
146+
query.append("ssl-key={0}".format(kwargs["ssl_key"]))
147+
148+
if len(query) > 0:
149+
uri = "{0}?{1}".format(uri, "&".join(query))
150+
151+
return uri
152+
92153

93154
@unittest.skipIf(tests.MYSQL_VERSION < (5, 7, 12), "XPlugin not compatible")
94155
class MySQLxXSessionTests(tests.MySQLxTests):
@@ -117,15 +178,16 @@ def test___init__(self):
117178

118179
# XSession to a farm using one of many routers (prios)
119180
# Loop during connect because of network error (succeed)
120-
uri = ("mysqlx://{0}:{1}@[(address=bad_host, priority=100),"
121-
"(address={2}:{3}, priority=98)]"
122-
"".format(user, password, host, port))
181+
routers = [{"host": "bad_host","priority": 100},
182+
{"host": host, "port": port, "priority": 98}]
183+
uri = build_uri(user=user, password=password, routers=routers)
123184
session = mysqlx.get_session(uri)
124185
session.close()
125186

126187
# XSession to a farm using one of many routers (incomplete prios)
127-
uri = ("mysqlx://{0}:{1}@[(address=bad_host, priority=100), {2}:{3}]"
128-
"".format(user, password, host, port))
188+
routers = [{"host": "bad_host", "priority": 100},
189+
{"host": host, "port": port}]
190+
uri = build_uri(user=user, password=password, routers=routers)
129191
self.assertRaises(mysqlx.errors.ProgrammingError,
130192
mysqlx.get_session, uri)
131193
try:
@@ -134,9 +196,9 @@ def test___init__(self):
134196
self.assertEqual(4000, err.errno)
135197

136198
# XSession to a farm using invalid priorities (out of range)
137-
uri = ("mysqlx://{0}:{1}@[(address=bad_host, priority=100), "
138-
"(address={2}:{3}, priority=101)]"
139-
"".format(user, password, host, port))
199+
routers = [{"host": "bad_host", "priority": 100},
200+
{"host": host, "port": port, "priority": 101}]
201+
uri = build_uri(user=user, password=password, routers=routers)
140202
self.assertRaises(mysqlx.errors.ProgrammingError,
141203
mysqlx.get_session, uri)
142204
try:
@@ -145,19 +207,18 @@ def test___init__(self):
145207
self.assertEqual(4007, err.errno)
146208

147209
# Establish an XSession to a farm using one of many routers (no prios)
148-
uri = ("mysqlx://{0}:{1}@[bad_host, {2}:{3}]"
149-
"".format(user, password, host, port))
210+
routers = [{"host": "bad_host"}, {"host": host, "port": port}]
211+
uri = build_uri(user=user, password=password, routers=routers)
150212
session = mysqlx.get_session(uri)
151213
session.close()
152214

153215
# Break loop during connect (non-network error)
154-
uri = ("mysqlx://{0}:{1}@[bad_host, {2}:{3}]"
155-
"".format(user, "bad_pass", host, port))
216+
uri = build_uri(user=user, password="bad_pass", routers=routers)
156217
self.assertRaises(mysqlx.errors.InterfaceError,
157218
mysqlx.get_session, uri)
158219

159220
# Break loop during connect (none left)
160-
uri = "mysqlx://{0}:{1}@[bad_host, another_bad_host]"
221+
uri = "mysqlx://{0}:{1}@[bad_host, another_bad_host]".format(user, password)
161222
self.assertRaises(mysqlx.errors.InterfaceError,
162223
mysqlx.get_session, uri)
163224
try:
@@ -211,12 +272,11 @@ def test_mysqlx_socket(self):
211272

212273

213274
def test_connection_uri(self):
214-
uri = ("mysqlx://{user}:{password}@{host}:{port}/{schema}"
215-
"".format(user=self.connect_kwargs["user"],
275+
uri = build_uri(user=self.connect_kwargs["user"],
216276
password=self.connect_kwargs["password"],
217277
host=self.connect_kwargs["host"],
218278
port=self.connect_kwargs["port"],
219-
schema=self.connect_kwargs["schema"]))
279+
schema=self.connect_kwargs["schema"])
220280
session = mysqlx.get_session(uri)
221281
self.assertIsInstance(session, mysqlx.XSession)
222282

@@ -361,16 +421,23 @@ def test_ssl_connection(self):
361421

362422
session.close()
363423

364-
uri = ("mysqlx://{0}:{1}@{2}?ssl-ca={3}&ssl-cert={4}&ssl-key={5}"
365-
"".format(config["user"], config["password"], config["host"],
366-
quote(config["ssl-ca"]), quote(config["ssl-cert"]),
367-
quote(config["ssl-key"])))
424+
ssl_ca="{0}{1}".format(config["ssl-ca"][0],
425+
quote_plus(config["ssl-ca"][1:]))
426+
ssl_key="{0}{1}".format(config["ssl-key"][0],
427+
quote_plus(config["ssl-key"][1:]))
428+
ssl_cert="{0}{1}".format(config["ssl-ca"][0],
429+
quote_plus(config["ssl-cert"][1:]))
430+
uri = build_uri(user=config["user"], password=config["password"],
431+
host=config["host"], ssl_ca=ssl_ca,
432+
ssl_cert=ssl_cert, ssl_key=ssl_key)
368433
session = mysqlx.get_session(uri)
369434

370-
uri = ("mysqlx://{0}:{1}@{2}?ssl-ca=({3})&ssl-cert=({4})&ssl-key=({5})"
371-
"".format(config["user"], config["password"], config["host"],
372-
config["ssl-ca"], config["ssl-cert"],
373-
config["ssl-key"]))
435+
ssl_ca = "({0})".format(config["ssl-ca"])
436+
ssl_cert = "({0})".format(config["ssl-cert"])
437+
ssl_key = "({0})".format(config["ssl-key"])
438+
uri = build_uri(user=config["user"], password=config["password"],
439+
host=config["host"], ssl_ca=ssl_ca,
440+
ssl_cert=ssl_cert, ssl_key=ssl_key)
374441
session = mysqlx.get_session(uri)
375442

376443

@@ -395,12 +462,11 @@ def test___init__(self):
395462
self.assertRaises(TypeError, mysqlx.NodeSession, bad_config)
396463

397464
def test_connection_uri(self):
398-
uri = ("mysqlx://{user}:{password}@{host}:{port}/{schema}"
399-
"".format(user=self.connect_kwargs["user"],
400-
password=self.connect_kwargs["password"],
401-
host=self.connect_kwargs["host"],
402-
port=self.connect_kwargs["port"],
403-
schema=self.connect_kwargs["schema"]))
465+
uri = build_uri(user=self.connect_kwargs["user"],
466+
password=self.connect_kwargs["password"],
467+
host=self.connect_kwargs["host"],
468+
port=self.connect_kwargs["port"],
469+
schema=self.connect_kwargs["schema"])
404470
session = mysqlx.get_node_session(uri)
405471
self.assertIsInstance(session, mysqlx.NodeSession)
406472

@@ -535,14 +601,21 @@ def test_ssl_connection(self):
535601

536602
session.close()
537603

538-
uri = ("mysqlx://{0}:{1}@{2}?ssl-ca={3}&ssl-cert={4}&ssl-key={5}"
539-
"".format(config["user"], config["password"], config["host"],
540-
quote(config["ssl-ca"]), quote(config["ssl-cert"]),
541-
quote(config["ssl-key"])))
604+
ssl_ca="{0}{1}".format(config["ssl-ca"][0],
605+
quote_plus(config["ssl-ca"][1:]))
606+
ssl_key="{0}{1}".format(config["ssl-key"][0],
607+
quote_plus(config["ssl-key"][1:]))
608+
ssl_cert="{0}{1}".format(config["ssl-ca"][0],
609+
quote_plus(config["ssl-cert"][1:]))
610+
uri = build_uri(user=config["user"], password=config["password"],
611+
host=config["host"], ssl_ca=ssl_ca,
612+
ssl_cert=ssl_cert, ssl_key=ssl_key)
542613
session = mysqlx.get_node_session(uri)
543614

544-
uri = ("mysqlx://{0}:{1}@{2}?ssl-ca=({3})&ssl-cert=({4})&ssl-key=({5})"
545-
"".format(config["user"], config["password"], config["host"],
546-
config["ssl-ca"], config["ssl-cert"],
547-
config["ssl-key"]))
615+
ssl_ca = "({0})".format(config["ssl-ca"])
616+
ssl_cert = "({0})".format(config["ssl-cert"])
617+
ssl_key = "({0})".format(config["ssl-key"])
618+
uri = build_uri(user=config["user"], password=config["password"],
619+
host=config["host"], ssl_ca=ssl_ca,
620+
ssl_cert=ssl_cert, ssl_key=ssl_key)
548621
session = mysqlx.get_node_session(uri)

0 commit comments

Comments
 (0)