【谨以此文,告诉大家,不会写代码也能做开发】

近日接到这么个需求,给Zabbix添加个交互。该交互首先需要给主机添加个自定义的ID,然后再在主机监测中,跳转到第三方系统,中间媒介就是自定义的ID。

需求便是如此,要做的事情就是:

1、在数据库中找到主机对应的表,添加字段,这里我们称之为deviceId;

2、在页面上增加写入的地方,然后让它保存到数据库中。

以这个需求为例,我们来看下Zabbix后台管理系统的大致架构。然后看下我又一次踩的坑。

首先要明确,Zabbix是一个设备管理的开源系统,且其后台管理系统是用php写的。

第一步,我们找到添加主机的页面,这里有创建主机的地方

zabbix源码编译参数 zabbix源码分析_程序人生

观察其URL,访问的是hosts.php文件,由此可见,这部分代码在hosts.php文件中。

第二步:我们顺势打开hosts.php文件,一看,我勒个去,浩浩汤汤,1600多行代码!先不急着哭,我们发现URL里面还有个参数:form=create。我们把代码按分支收起来,找到(hasRequest('form'))的分支。这代码这么长,主要里面有很多if,我并非一个php的工程师,所以并不好说这种写法的优劣性。

第三步:在(hasRequest('form'))的分支内,一顿操作猛如虎啊,眼花缭乱,根本不知道做的啥跟啥,做完一大堆操作之后,最后来到一句话:

$hostView = new CView('configuration.host.edit', $data);

约莫猜着,这里返回了个视图。第一个参数目测是视图参数,在VSCode中一搜,发现这个似乎是个文件。于是我们打开了configuration.host.edit.php

【插播】

这里顺带说下,Zabbix里面的页面,基本没什么模板,也看不到多少html,清一色都是靠CView CDiv这些类给凑出来的。很有一种用C# Java写桌面程序的既视感。再不济也像写APP。厉害了我的PHP。

第四步:此时我们已经探索到了configuration.host.edit.php,大致观察了下,做了很多addRow的事情。直觉可以判断出,这里就是添加form表单的地方。这里我使出了多年的功力,“不会写代码也能开发”的绝技,我抄了一行。

->addRow(
		(new CLabel(_('Host name'), 'host'))->setAsteriskMark(),
		(new CTextBox('host', $data['host'], $data['readonly'], 128))
			->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
			->setAriaRequired()
			->setAttribute('autofocus', 'autofocus')
	)

然后刷新页面(PHP就是这个好处,可以直接刷新),成功了,页面多出了一行元素,多了个输入框textbox。观察了下,里面的$data['host']就是要获取值用的。

第五步:

找到数据库,然后给对应的表增加个字段。Zabbix用的是MySQL,然后主机就是hosts,于是很容易找到hosts表,然后根据已有数据做出判断就是它,用sql语句添加个字段。并非难事,不细说了。

第六步:

是不是这样就可以在textbox里面写入新字段的内容,然后就能顺利地把新字段的填到数据库了呢?事实表明,我太年轻了。不然的话,就没来由这篇文章对Zabbix的后台管理系统进行浅析了。

当我发现并没有写进去的时候,我只能一步一步地跟着这份冗长的php代码步入进去。没办法,我既不是PHP工程师,也没研究过Zabbix,只能傻瓜式去深入。

第七步:

从业经验告诉我,点击保存是会触发某些动作的。回到hosts.php,发现这个动作就是‘add’,于是又找到了这部分代码的分支

elseif (hasRequest('add') || hasRequest('update')) {
	try {
		DBstart();

		$hostId = getRequest('hostid', 0);

……………………

            // Host data.
			$host = [
				'host' => getRequest('host'),
				'name' => getRequest('visiblename'),
				'status' => getRequest('status', HOST_STATUS_NOT_MONITORED),
				'description' => getRequest('description'),
				'proxy_hostid' => getRequest('proxy_hostid', 0),
				'ipmi_authtype' => getRequest('ipmi_authtype'),
				'ipmi_privilege' => getRequest('ipmi_privilege'),
				'ipmi_username' => getRequest('ipmi_username'),
				'ipmi_password' => getRequest('ipmi_password'),
				'tls_connect' => getRequest('tls_connect', HOST_ENCRYPTION_NONE),
				'tls_accept' => getRequest('tls_accept', HOST_ENCRYPTION_NONE),
				'groups' => zbx_toObject($groups, 'groupid'),
				'templates' => $templates,
				'interfaces' => $interfaces,
				'tags' => $tags,
				'macros' => $macros,
				'inventory_mode' => getRequest('inventory_mode'),
				'inventory' => (getRequest('inventory_mode') == HOST_INVENTORY_DISABLED)
					? []
					: getRequest('host_inventory', [])
			];

其中有一块很整齐的代码,就是把数据填入$host变量的。那我把这个填入不就完事了吗?满怀兴奋地做了测试,呵呵,我还是太年轻了,果然没写进去。我真是个LJ。

第八步:

来嘛,不服就干。上面填入$host变量之后,接下来干了什么事情呢?有句关键的代码

$hostIds = API::Host()->create($host);

喏,这不就是插入一条数据嘛。但这个,按照我写JS的习惯,$host里面的数据只要有了,应该都能写进去才对。为啥就写不进去的呢?只能想办法观察SQL语句了。接下来的问题又出现了,我去哪看这个sql语句呢?create函数在哪?

第九步:

我只知道,正常架构应该是要有类似于DAO层的东西,果然就看到了CHost的这个类,里面有Create方法。这个类的实例是有API这个工厂统一创建的。文件在API.php。

第十步:

Create方法里面的核心代码:

$hostid = DB::insert('hosts', [$host]);

这里就是调用DB的写入方法了。看来功夫不负有心人,快找到答案了。

直到来到这一层,$host变量的数据都是很正常的。我们要的自定义属性都是ok的。你说气人不气人,非得让我去到最后一层。

第11步:

既然如此,那就去到DB.php文件中的insert方法,把sql语句打印出来。

这个时候就呵呵了,sql语句中果然没有帮我把自定义的字段给插进去。

 

第12步:

观察下这部分代码:

foreach ($values as $key => $row) {
			if ($getids) {
				$resultIds[$key] = $id;
				$row[$tableSchema['key']] = $id;
				$id = bcadd($id, 1, 0);
			}

			self::checkValueTypes($table, $row);

			$sql = 'INSERT INTO '.$table.' ('.implode(',', array_keys($row)).')'.
					' VALUES ('.implode(',', array_values($row)).')';

			if (!DBexecute($sql)) {
				self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s".', $sql));
			}
		}

这里的操作就是一个sql语句拼接的过程,其中$row变量就是存储了字段信息。分别在不同的阶段观察了下$row的内容,发现了在checkValueTypes之前,$row还保留了我的自定义字段,checkValueTypes之后,就消失了!

答案已经揭晓,必然是checkValueTypes做了什么对不住我的事情。你个负心婆娘。

第13步:

前往checkValueTypes捉奸。

大致看下这个代码:

public static function checkValueTypes($table, &$values) {
		global $DB;
		$tableSchema = self::getSchema($table);


		foreach ($values as $field => $value) {
			if (!isset($tableSchema['fields'][$field])) {
				unset($values[$field]);
				continue;
			}

			if (isset($tableSchema['fields'][$field]['ref_table'])) {
				if ($tableSchema['fields'][$field]['null']) {
					$values[$field] = ($values[$field] == '0') ? NULL : $values[$field];
				}
			}

(太长懒得copy)

这里大概的意思就是判断字段是否有效。而且还做了个操作:

$tableSchema = self::getSchema($table);

大致可以判断是根据表名获取了数据结构。看来捉奸只来到了酒店前台,还没到房间。那就去getSchema这个房间看看。

第14步:

大致看下代码:

public static function getSchema($table = null) {
		if (is_null(self::$schema)) {
			self::$schema = include(dirname(__FILE__).'/../../'.self::SCHEMA_FILE);
		}

		if (is_null($table)) {
			return self::$schema;
		}
		elseif (isset(self::$schema[$table])) {
			return self::$schema[$table];
		}
		else {
			self::exception(self::SCHEMA_ERROR, _s('Table "%1$s" does not exist.', $table));
		}
	}

这里就拉取了一个文件,文件名定义在self::SCHEMA_FILE里面。摸到这个文件的定义:

const SCHEMA_FILE = 'schema.inc.php';

然后打开这个文件,豁然开朗,原来所有表结构的定义都在这个文件里面,如果没定义在这个文件里面的字段,就会在checkValueTypes被过滤掉,从而写不进去!

于是,就在'schema.inc.php'加上我想要的字段,刷新,执行测试,再看数据库,终于都写进去了。

 

长吁一口气。

 

此次的探查,大致摸清了Zabbix的代码结构。以host为例,自顶向下大致如下分层:

hosts.php  ----controller层

configuration.host.edit.php  ----view层

CHost.php----service层,API是实现了工厂模式

DB.php----util层,db用于数据库交互。

schema.inc.php---配置层,用于配置信息。

其他不同的文件可以大致对应过去。

 

这就是今天我作为一个非PHP的程序员,用最原始的echo和print_r,活生生地抄出了个功能。手段简陋到我都不太好意思,十分唏嘘。最后说一句:PHP是这个世界上最好的语言。

 

----------------------后记-------------------------

后来同事在我这个基础上去修改,还是没成功,检查了下还少了个地方

zabbix源码编译参数 zabbix源码分析_恰饭_02

 

 

 

【当你看到这,说明你还是很感兴趣,码字不易,点个赞点个关注再走呗,哈哈】