我花了一天左右的时间研究这个问题,并且认为如果不对驱动程序代码进行重大更改,或者为旧版本创建极其困难的构建环境,这是不可能的。
我把它放在一个答案中,这样其他人就不会掉进我做过的同一个兔子洞里(或者,更好的是,这样其他人就可以从我离开的地方继续并真正解决问题!).. .它不适合评论。
这将是一个有点大部头,对不起。
概述
我能够使用一对 Ubuntu 16.04 容器、Oracle 提供的 MySQL 3.23 download 以及所有客户端库来重现您在帖子中提到的每个错误情况(感谢您提出的全面而出色的问题!)你提到的,还有一些其他的。
以下是我在尝试在您提到的每个地方找到其他解决方案时发现的内容,然后是一些“下一步”类型的信息和一些关于故事寓意的宣传。
所有这些测试均使用最新版本的 Python 2、UnixODBC 和 pyodbc(通过 pip)进行,截至 2017 年 11 月 26 日,库存 Ubuntu 16.04 Docker 容器可用。
所有使用的 URL 都是链接的,但是,如果历史有任何迹象,它们可能会随着时间的推移而消失,考虑到该软件中的很多都已接近 20 年。如果您愿意,我也很乐意发布我的任何/所有 shellscripts/Dockerfiles/修改过的驱动程序源;只需在 cmets 中 ping 我即可。
old_password.so 和 MariaDB 连接器/ODBC 3.0.2
您说得对,这是最有潜力的故障排除选项。这是我所做的:
首先,我安装了 Connector/ODBC 3.0.2 二进制文件并尝试通过 Python 连接到它。在为名为“maria”的数据源配置我的 ODBC .ini 文件后,我遇到了同样的错误,即:
> pyodbc.connect('DRIVER={maria};Server=mysql;Database=mysql;User=admin;Password=admin')
pyodbc.Error: ('HY000', u'[HY000] [unixODBC][ma-3.0.2]Plugin old_password could not be loaded: lib/mariadb/plugin/old_password.so: cannot open shared object file: No such file or directory (2059) (SQLDriverConnect)')
当 ODBC 代码与宣布足够老的身份验证协议的 MySQL 服务器一起出现时,它会尝试加载为 Connector/C MariaDB driver 构建的已编译插件。 straceing ODBC 连接尝试的输出确定了这一点。
old_password.so 原来是 Connector/C MariaDB 驱动程序的一个组件,但不是该驱动程序的二进制版本中包含的库。很有趣。
原来有一堆类似于old_password的插件模块包含在Connector/C驱动的源代码中。我下载了Connector/C 3.0.2 sources 并打开了那些以.so 文件分发的“auth”类型插件的文档、源代码和构建系统,看看我能找到什么。
我发现,Connector/C 的各种组件既可以编译为“静态”链接到主驱动程序库的插件,也可以编译为动态库本身。我用引号说“静态”,因为 C 驱动程序的构建过程会创建 mariadbclient 的静态 (.a) 和动态 (.so) 版本,但如果特定插件在构建中声明为静态系统,该插件的代码静态包含在两个 mariadbclient 工件中。
old_password.so 文件的源代码似乎位于 plugins/auth/old_password.c 的单个小源文件中。
似乎可以更改构建系统 (CMake) 来为 old_password 插件生成动态库。在连接器/C 源中有一个cmake/plugins.cmake 文件,它充当所有插件的“注册表”。它包含一个 cmake 宏 REGISTER_PLUGIN,它采用 STATIC 或 DYNAMIC 参数。我在该文件中搜索old_password 并找到以下行:
REGISTER_PLUGIN("AUTH_OLDPASSWORD" "${CC_SOURCE_DIR}/plugins/auth/old_password.c" "old_password_client_plugin" "STATIC" "" 0)
这看起来很有希望。 确实为他们的插件生成.so文件的类似行建模,我将该行更改为以下内容并运行构建:
REGISTER_PLUGIN("AUTH_OLDPASSWORD" "${CC_SOURCE_DIR}/plugins/auth/old_password.c" "old_password_client_plugin" "DYNAMIC" "old_password" 1)
由于缺少依赖项,构建失败了几次。我不得不安装一些-dev 包和其他工具,但最后我能够干净地构建(对于插件,事实证明你不需要 CURL 或 OpenSSL)。果然,在plugins/auth 目录中创建了一个名为mysql_old_password.so 的文件作为构建工件。
- 现在,我需要我的 Python 代码来找到那个插件;它仍然给了我关于未能找到lib/mariadb/plugin/old_password.so 的错误。我将您在问题中提到的PLUGIN_DIR 参数提供给ODBC 连接字符串,将我编译的mysql_old_password.so 重命名为old_password.so,并运行以下代码。 . .并得到一个新的错误!进步!
conn = pyodbc.connect('DRIVER={maria};Server=mysql;Database=mysql;User=admin;Password=admin;PLUGIN_DIR=/home/mysql/zclient/mdb-c/plugins/auth')
pyodbc.Error: ('HY000', u'[HY000] [unixODBC][ma-3.0.2]Plugin old_password could not be loaded: /home/mysql/zclient/mdb-c/plugins/auth/old_password.so: undefined symbol: ma_scramble_323 (2059) (SQLDriverConnect)')
看起来编译的工件已损坏,缺少ma_scramble_323 函数定义。由于插件是在运行时动态加载的,程序仍然会启动,但是当它尝试dload 插件时它会爆炸。更糟糕的是,该函数看起来像是“旧” MySQL 协议身份验证机制的主要密码哈希入口点,所以我不能放弃它。在 Connector/C 源代码中,我找到了该函数的声明和标头 (mariadb_com.h),但 includeing 在 old_password.c 源文件的各个位置似乎并没有起到作用。我的直觉是,这是两种不幸行为的相互作用。首先,由 Connector/C 构建系统编译的插件被设置为假设它们将仅由 Connector/C 插件链接,或类似的东西。这意味着插件本身在编译时不会链接到“通用”连接器/C 功能,因为这些东西应该已经在加载插件的东西中可用。由于我们使用的是连接器/ODBC,而不是连接器/C,因此这些常用功能不存在或无法访问。现在,从源代码构建连接器/ODBC 需要连接器/C,因此可以编译一个新的连接器/ODBC 库,使其包含正确的功能,但我不想从那个兔子洞开始。其次,即使被告知在独立(不要编译其他任何东西)模式下构建old_password 插件,CMake 的依赖分析也没有发现或链接描述ma_scramble_323 的文件。这可能是一个 CMake 问题,但可能是因为构建系统没有像上面提到的那样考虑这个用例。
在这里,我很幸运。 ma_scramble_323 函数在 libmariadb/ma_password.c 中定义,这是一个非常小、简单的源文件,与 old_password 插件尚未依赖的 Connector/C 项目中的任何其他库没有重大依赖关系。我做了“穷人的链接”(恶心),只是将ma_scramble_323 函数的源代码复制到old_password.c 文件中。这些函数在ma_password.c 文件中调用了其他函数,所以我将它们复制到。同样,这很容易(或根本不是一个选项),因为ma_password.c 文件非常简单。如果它本身具有依赖关系或更复杂,我将不得不停止、放弃并学习高级 CMake-fu 以“正确”地解决问题。我绝对肯定有更好的方法来做到这一点。
(旁白)此时我不得不在我的数据库服务器上定期运行mysqladmin flush-hosts,因为我的测试导致了如此多的失败尝试,以至于我不得不经常这样做。也可能有更好的解决方法,但我不知道,我知道 cron。
使用新的“内联”源,编译 mysql_old_password.so 库,我重命名它,并再次运行我的测试脚本。这一次,我得到了:
pyodbc.Error: ('HY000', u'[HY000] [unixODBC][ma-3.0.2]Plugin old_password could not be loaded: name mismatch (2059) (SQLDriverConnect)')
我认为这与我正在重命名文件以便 ODBC 可以找到它(它正在寻找 old_password.so 而不是 mysql_old_password.so)这一事实有关。我尝试了霰弹枪的方法。在plugins/auth/CMakeLists.txt 构建系统配置中,我将mysql_old_password 的所有实例替换为old_password 并编译。编译成功,但还是不行。
事实证明,插件源本身(在这种情况下为old_password.c)在顶部有一个结构声明,宣布了他们的名字,而这个宣布它的名字为mysql_old_password。这很可能是一个预先存在的问题(即这从来没有工作过),我开始感到有点寒意:当您构建的代码感觉就像以前没有人在给定的配置中构建或测试过它时,你成功的几率不大。无论如何,我在源文件上也做了同样的s/mysql_old_password/old_password/,并进行了编译。这次它生成了一个具有正确 old_password.so 名称的工件。我再次运行我的测试脚本并得到:
conn = pyodbc.connect('DRIVER={maria};Server=mysql;Database=mysql;User=admin;Password=admin;PLUGIN_DIR=/home/mysql/zclient/mdb-c/plugins/auth')
pyodbc.Error: ('HY000', u"[HY000] [unixODBC][ma-3.0.2]Access denied for user: 'admin@hostname' (Using password: NO) (1045) (SQLDriverConnect)")
这很奇怪。我在我的客户端测试框中也安装了 3.23 服务器附带的mysql 命令行客户端(通过 tarball,而不是在系统库路径中),它可以与这些凭据很好地连接(我无法使用isql 因为我无法让它正确使用 PLUGIN_DIR 并且无法弄清楚它希望我将插件放在哪里;它不在系统 /usr 目录中,也不是相对目录)。我想不出办法。我已经为每个数据库、admin 用户和同名密码设置了所有通常的“超混杂、仅测试”GRANTs、localhost 和 % 的 MySQL 服务器。
我放弃并将密码设置为空/null,禁用密码验证,确保我仍然可以在命令行上通过mysql登录,并尝试最后一次:
pyodbc.Error: ('HY000', u'[HY000] [unixODBC][ma-3.0.2]Error in server handshake (2012) (SQLDriverConnect)')
这被证明是丧钟。在研究这个错误时,我发现了this GitHub issue,人们似乎非常确信这代表了基本的客户端/服务器协议不兼容。在这一点上,我放弃了old_password.so 方法。 MariaDB 驱动程序代码(C 或 ODBC)的 3.0.2 版本似乎无法使用足够古老的 MySQL 协议方言来工作,尽管在该过程中我可能错过了很多可能的修复。
尝试了其他路径
我尝试了您在问题中提到的其他一些事情,我将在这里简要介绍一下:
您可能已经发现,尝试在 MariaDB 2.0 ODBC 驱动程序系列中禁用 SQL_AUTO_IS_NULL 行为效果不佳。 This bug thread 和 ODBC Connector parameters list 对如何禁用该字段的设置有一些建议(Option=8388608 很明显,对吗?),但这些强制禁用或启用标志的尝试都没有改变行为,无论它们是在连接字符串中还是在 ODBC .ini 文件中。
MySQL archive site 有旧版本的 ODBC 连接器可用。不幸的是,他们所有的编译版本都是针对 32 位 Linux 的,而我没有。我尝试从源代码构建,即使配置工具链也是一项繁重的工作。在我不得不hand-install system identification files from 1999 的时候,我知道这可能是一个失败的原因,但我安装了所有的 deps 和古老的版本并尝试编译它。编译错误的数量和多样性使我放弃了这种方法(C 标准不匹配,加上与 UnixODBC 的几乎所有部分缺乏兼容性)。完全有可能对我错过的这些问题进行简单的修复;我不是 C 编码器或旧的 linux-build-system 专家。
我尝试了一些third party MySQL ODBC connectors,但没有成功;与 5.* 系列相同的错误。
我编译了 2.50.39 版本的 Connector/ODBC 库(存档中只有源可用)。为此,我首先为 3.23 版本的服务器编译了 libmysqlclient.so.10 文件。这需要更改 3.23 服务器的源以解决一些与errno 相关的问题(删除my_sys.h 中extern int errno 的#define 子句),将libtool 操作系统定义文件复制到源目录中的各个位置(@987654410 @ 被复制到 .、mit-pthreads 和 mit-pthreads/config/,如果重要的话)。之后,我能够使用--with-mit-threads --without-server --without-docs --without-bench 配置开关编译和构建libmysqlclient 库。在那之后评估mysql 客户端程序的宏时,编译失败并出现几个难以理解的错误,但是libmysqlclient 的.so 文件已经生成,所以我抓住它们并继续前进。 libmysqlclient.so.10 库编译完成后,我构建了2.50.39 version of Connector/ODBC from the archive。这需要更改主 MySQL 包含文件的一些源(删除对asm/atomic.h 的引用),以及与其他库相同的系统识别 libtool hack。它找不到iodbc 库(通过libiodbc2-dev 包安装在Ubuntu 上),因为它们现在位于/usr/include 而不是/usr/local/include。我终于用开关--with-mysql-includes=$path_to_3.23_mysql_binary_dir/include --with-mysql-libs=$path_to_compiled_libmysqlclient.so.10_files_from_mysql_server_3.23_sources --with-iodbc-includes=/usr/include/iodbc 配置了它,除了前面提到的atomic.h 问题之外,它的构建没有任何问题。然而,毕竟,通过我新编译的libmyodbc.so 连接导致了 Python/UnixODBC 中的段错误。 Valgrind、gdb 和其他工具无法确定原因;也许更精通调试编译库互操作性问题的人可以解决这个问题。
MySQL 存档具有旧的、二进制 RPM 版本的连接器/ODBC。它们都是 32 位的,几乎所有现代 Linux 都是 64 位的。我通过安装i386 架构和所需的库来尝试shimming 这些文件。 64 位 Python/UnixODBC 无法成功加载 myodbc 插件,返回一般的“找不到文件”错误,我最终将其追溯到对 dlopen 的失败调用。 Libtool 的 dlopen 包装器(由 UnixODBC 使用)被大多数人认为不是非常可调试的,并且在经历了很大的麻烦之后,我的基本 Valgrind 技巧似乎表明,正如我所料,它不可能动态加载架构 -不兼容(i386 与 x86-64)ODBC 后端。
解决方案/剩余选项
一般来说,最终重写代码可能会更容易。例如,您可以制作一个包装旧版 Python 非 ODBC MySQL 驱动程序的 Python 模块(正如问题的 cmets 中建议的 @FlipperPA),将 pyodbc 接口的“足够”破解到您没有的模块上必须重构调用它的太多代码,并在部署之前进行彻底测试。我知道这很糟糕而且很冒险,但这可能是你最好的选择。在编写这样一个模块时,您可以利用pyodbc 中处理通用 ODBC 语法等的一些内部代码。
您甚至可以为 pyodbc 开发一个“假”的 ODBC 后端,它只是调用一个非 ODBC Python MySQL 驱动程序,但我怀疑这会很困难,因为 pyodbc 的后端可插入性似乎主要面向编译库而不是“虚拟”垫片代码。
我不是这方面的专家,所以我完全可能错过了一些解决方案!
我考虑并放弃了其他一些可能性:
您可以向 MariaDB 人员提交一个错误,它可能会被修复。我不太清楚我最终遇到的协议错误是“这在每个级别上都根本不兼容”还是“身份验证系统只需要调整一下就可以了”。可能值得一试。
由于 2.50 版连接器/ODBC 代码提供 32 位 RPM(它们不会加载到 64 位 Python/UnixODBC 环境中),因此您可以转换整个堆栈(甚至操作系统分发)到 32 位代码。但是,如果您使用任何不常见的编译内容,这可能会很麻烦。虽然 Ubuntu/Debian 尤其擅长在旧架构上提供软件包,但这仍然可能很棘手。即使你把所有东西都转换了,一些行为可能会改变,而且旧的 32 位特性对于任何在你的应用程序上工作的人来说都是一个持续的陌生感。只有当 2.50 驱动程序在从 32 位运行时访问时才能工作;之后可能还会出现其他问题。如果您的所有客户端代码的维护负担在未来可能非常低(如果项目很小或不太可能更改),我只建议尝试此操作。
故事的寓意
软件腐烂得血腥快。除非项目持续致力于保持向后兼容性,否则事情将很快停止工作,尤其是在网络软件中。
并不是产品本身坏了,而是宇宙从它下面以一百万种微小的方式发生了变化。除非有人足够通才并且已经存在足够长的时间来了解所有这些小变化以及如何扭转它们,否则很难将所有内容及时/修订后移到“正常工作”的地方。
如果您获得某些东西的二进制文件,即使它是所谓的“常见”的东西,例如 MySQL 驱动程序,保留它们。理想情况下,将它们与互联网共享。
如果您有某些东西的来源,请严格记录他们需要的整个依赖项/工具链列表,并为人类记录下来。假设需要读取程序依赖列表的工具,例如自动工具本身就会过时。没有什么太“明显”而无法记录;不是架构、内核 ABI、libc 行为——什么都没有。既然我们拥有像 Docker 这样的“在任何内核上的盒子”,您也许能够以编程方式存储更多的依赖项,但不要指望它。