implemented syslog proxy in agent (issue #568)
authorVictor Kirhenshtein <victor@netxms.org>
Sun, 28 Aug 2016 14:14:02 +0000 (17:14 +0300)
committerVictor Kirhenshtein <victor@netxms.org>
Sun, 28 Aug 2016 14:14:02 +0000 (17:14 +0300)
17 files changed:
ChangeLog
include/netxmsdb.h
include/nms_cscp.h
sql/setup.in
src/agent/core/Makefile.am
src/agent/core/nxagentd.cpp
src/agent/core/nxagentd.h
src/agent/core/snmptrapproxy.cpp
src/agent/core/syslog.cpp [new file with mode: 0644]
src/server/core/agent.cpp
src/server/core/main.cpp
src/server/core/node.cpp
src/server/core/syslogd.cpp
src/server/include/nms_objects.h
src/server/include/nxsrvapi.h
src/server/libnxsrv/agent.cpp
src/server/tools/nxdbmgr/upgrade.cpp

index dc20cf7..d0c8ba7 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
 - Zone ID can be set for agent in SNMP proxy mode
 - Zones has common default proxy node for all protocols
 - Zone's proxy node can be placed inside that zone
+- Syslog proxy in agent
 - Built-in superuser account renamed to "system"
 - Default "admin" account now is ordinary member of "Admins" group without built-in privileges
 - Management console
        - New editors for Agent Config Policy and Log Parser Policy. 
        - DCI summary tables with empty menu path not shown in object context menu
        - Fixed glitches in table value view
-- Fixed issues: #92, #851, #906, #909, #942, #959, #992, #999, #1006, #1051, #1096, #1100, #1159, #1187, #1191, #1230, #1237, #1245, #1254, #1261, #1263, #1273, #1275
+- Fixed issues: #92, #568, #851, #906, #909, #942, #959, #992, #999, #1006, #1051, #1096, #1100, #1159, #1187, #1191, #1230, #1237, #1245, #1254, #1261, #1263, #1273, #1275
 
 
 *
index c5ebec6..a35c612 100644 (file)
@@ -23,6 +23,6 @@
 #ifndef _netxmsdb_h
 #define _netxmsdb_h
 
-#define DB_FORMAT_VERSION   413
+#define DB_FORMAT_VERSION   414
 
 #endif
index 1f6afab..04c0721 100644 (file)
@@ -1133,6 +1133,7 @@ typedef struct
 #define VID_SSH_PASSWORD            ((UINT32)547)
 #define VID_SSH_PROXY               ((UINT32)548)
 #define VID_ZONE_PROXY              ((UINT32)549)
+#define VID_MESSAGE_LENGTH          ((UINT32)550)
 
 // Base variabe for single threshold in message
 #define VID_THRESHOLD_BASE          ((UINT32)0x00800000)
index 23a9c6b..81b3b01 100644 (file)
@@ -62,7 +62,7 @@ INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('
 INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('EnableNXSLContainerFunctions','1',1,1);
 INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('EnableObjectTransactions','0',1,1);
 INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('EnableSNMPTraps','1',1,1);
-INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('EnableSyslogDaemon','0',1,1);
+INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('EnableSyslogReceiver','0',1,1);
 INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('EnableTimedAlarmAck','1',1,1);
 INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('EnableXMPPConnector','0',1,1);
 INSERT INTO config (var_name,var_value,is_visible,need_server_restart) VALUES ('EnableZoning','0',1,1);
index 14fc0da..12262e2 100644 (file)
@@ -5,8 +5,8 @@ nxagentd_SOURCES = messages.c actions.cpp appagent.cpp comm.cpp config.cpp \
                    exec.cpp extagent.cpp getparam.cpp localdb.cpp master.cpp \
                    nxagentd.cpp policy.cpp push.cpp register.cpp sa.cpp \
                    sd.cpp session.cpp snmpproxy.cpp snmptrapproxy.cpp \
-                   static_subagents.cpp subagent.cpp sysinfo.cpp tools.cpp \
-                   trap.cpp upgrade.cpp watchdog.cpp
+                   static_subagents.cpp subagent.cpp sysinfo.cpp syslog.cpp \
+                  tools.cpp trap.cpp upgrade.cpp watchdog.cpp
 if USE_INTERNAL_EXPAT
 nxagentd_LDADD = ../../appagent/libappagent.la ../libnxagent/libnxagent.la @top_srcdir@/src/db/libnxdb/libnxdb.la @top_srcdir@/src/libnetxms/libnetxms.la @top_srcdir@/src/snmp/libnxsnmp/libnxsnmp.la @top_srcdir@/src/libexpat/libexpat/libnxexpat.la @SUBAGENT_LIBS@
 else
index 5c8274c..f58b9e4 100644 (file)
@@ -63,10 +63,14 @@ THREAD_RESULT THREAD_CALL TrapSender(void *);
 THREAD_RESULT THREAD_CALL MasterAgentListener(void *arg);
 THREAD_RESULT THREAD_CALL SNMPTrapReceiver(void *);
 THREAD_RESULT THREAD_CALL SNMPTrapSender(void *);
+THREAD_RESULT THREAD_CALL SyslogReceiver(void *);
+THREAD_RESULT THREAD_CALL SyslogSender(void *);
 
 void ShutdownTrapSender();
 void ShutdownSNMPTrapSender();
 
+void ShutdownSyslogSender();
+
 void StartLocalDataCollector();
 void ShutdownLocalDataCollector();
 
@@ -167,6 +171,7 @@ UINT32 g_dcReconciliationBlockSize = 1024;
 UINT32 g_dcReconciliationTimeout = 15000;
 UINT32 g_dcMaxCollectorPoolSize = 64;
 UINT32 g_zoneId = 0;
+UINT16 g_syslogListenPort = 514;
 #ifdef _WIN32
 UINT16 g_sessionAgentPort = 28180;
 #else
@@ -204,6 +209,8 @@ static THREAD s_listenerThread = INVALID_THREAD_HANDLE;
 static THREAD s_eventSenderThread = INVALID_THREAD_HANDLE;
 static THREAD s_snmpTrapReceiverThread = INVALID_THREAD_HANDLE;
 static THREAD s_snmpTrapSenderThread = INVALID_THREAD_HANDLE;
+static THREAD s_syslogReceiverThread = INVALID_THREAD_HANDLE;
+static THREAD s_syslogSenderThread = INVALID_THREAD_HANDLE;
 static THREAD s_masterAgentListenerThread = INVALID_THREAD_HANDLE;
 static TCHAR s_processToWaitFor[MAX_PATH] = _T("");
 static TCHAR s_dumpDir[MAX_PATH] = _T("C:\\");
@@ -249,6 +256,7 @@ static NX_CFG_TEMPLATE m_cfgTemplate[] =
    { _T("EnableProxy"), CT_BOOLEAN, 0, 0, AF_ENABLE_PROXY, 0, &g_dwFlags, NULL },
    { _T("EnableSNMPProxy"), CT_BOOLEAN, 0, 0, AF_ENABLE_SNMP_PROXY, 0, &g_dwFlags, NULL },
    { _T("EnableSNMPTrapProxy"), CT_BOOLEAN, 0, 0, AF_ENABLE_SNMP_TRAP_PROXY, 0, &g_dwFlags, NULL },
+   { _T("EnableSyslogProxy"), CT_BOOLEAN, 0, 0, AF_ENABLE_SYSLOG_PROXY, 0, &g_dwFlags, NULL },
    { _T("EnableSubagentAutoload"), CT_BOOLEAN, 0, 0, AF_ENABLE_AUTOLOAD, 0, &g_dwFlags, NULL },
    { _T("EnableWatchdog"), CT_BOOLEAN, 0, 0, AF_ENABLE_WATCHDOG, 0, &g_dwFlags, NULL },
    { _T("EncryptedSharedSecret"), CT_STRING, 0, 0, MAX_SECRET_LENGTH, 0, g_szSharedSecret, NULL },
@@ -283,6 +291,7 @@ static NX_CFG_TEMPLATE m_cfgTemplate[] =
    { _T("SNMPTrapPort"), CT_LONG, 0, 0, 0, 0, &g_dwSNMPTrapPort, NULL },
    { _T("StartupDelay"), CT_LONG, 0, 0, 0, 0, &g_dwStartupDelay, NULL },
    { _T("SubAgent"), CT_STRING_LIST, '\n', 0, 0, 0, &m_pszSubagentList, NULL },
+   { _T("SyslogListenPort"), CT_WORD, 0, 0, 0, 0, &g_syslogListenPort, NULL },
    { _T("TimeOut"), CT_IGNORE, 0, 0, 0, 0, NULL, NULL },
    { _T("WaitForProcess"), CT_STRING, 0, 0, MAX_PATH, 0, s_processToWaitFor, NULL },
    { _T("ZoneId"), CT_LONG, 0, 0, 0, 0, &g_zoneId, NULL },
@@ -987,13 +996,18 @@ BOOL Initialize()
 
        s_eventSenderThread = ThreadCreateEx(TrapSender, 0, NULL);
 
-       // Start trap proxy threads(recieve and send), if trap proxy is enabled
-       if(g_dwFlags & AF_ENABLE_SNMP_TRAP_PROXY)
+       if (g_dwFlags & AF_ENABLE_SNMP_TRAP_PROXY)
        {
       s_snmpTrapSenderThread = ThreadCreateEx(SNMPTrapSender, 0, NULL);
       s_snmpTrapReceiverThread = ThreadCreateEx(SNMPTrapReceiver, 0, NULL);
    }
 
+   if (g_dwFlags & AF_ENABLE_SYSLOG_PROXY)
+   {
+      s_syslogSenderThread = ThreadCreateEx(SyslogSender, 0, NULL);
+      s_syslogReceiverThread = ThreadCreateEx(SyslogReceiver, 0, NULL);
+   }
+
        if (g_dwFlags & AF_SUBAGENT_LOADER)
        {
                s_masterAgentListenerThread = ThreadCreateEx(MasterAgentListener, 0, NULL);
@@ -1070,12 +1084,18 @@ void Shutdown()
                ThreadJoin(s_listenerThread);
        }
        ThreadJoin(s_eventSenderThread);
-       if(g_dwFlags & AF_ENABLE_SNMP_TRAP_PROXY)
+       if (g_dwFlags & AF_ENABLE_SNMP_TRAP_PROXY)
        {
       ShutdownSNMPTrapSender();
       ThreadJoin(s_snmpTrapReceiverThread);
       ThreadJoin(s_snmpTrapSenderThread);
        }
+   if (g_dwFlags & AF_ENABLE_SYSLOG_PROXY)
+   {
+      ShutdownSyslogSender();
+      ThreadJoin(s_syslogReceiverThread);
+      ThreadJoin(s_syslogSenderThread);
+   }
 
        DestroySessionList();
        MsgWaitQueue::shutdown();
index b1a9931..e5f3977 100644 (file)
 #define AF_DISABLE_IPV6             0x00100000
 #define AF_ENABLE_SNMP_TRAP_PROXY   0x00200000
 #define AF_BACKGROUND_LOG_WRITER    0x00400000
+#define AF_ENABLE_SYSLOG_PROXY      0x00800000
 
 // Flags for component failures
 #define FAIL_OPEN_LOG               0x00000001
@@ -643,6 +644,7 @@ extern UINT32 g_dwSNMPTrapPort;
 extern UINT32 g_longRunningQueryThreshold;
 extern UINT16 g_sessionAgentPort;
 extern UINT32 g_zoneId;
+extern UINT16 g_syslogListenPort;
 
 extern Config *g_config;
 
index 8eab909..34a4cce 100644 (file)
@@ -34,7 +34,7 @@ public:
 };
 
 /**
- * Static data
+ * Sender queue
  */
 static Queue s_snmpTrapQueue;
 
diff --git a/src/agent/core/syslog.cpp b/src/agent/core/syslog.cpp
new file mode 100644 (file)
index 0000000..1700b81
--- /dev/null
@@ -0,0 +1,317 @@
+/*
+** NetXMS multiplatform core agent
+** Copyright (C) 2014-2016 Raden Solutions
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+**
+** File: syslog.cpp
+**
+**/
+
+#include "nxagentd.h"
+
+#define MAX_SYSLOG_MSG_LEN    1024
+
+/**
+ * Syslog record
+ */
+class SyslogRecord
+{
+public:
+   InetAddress addr;
+   time_t timestamp;
+   char *message;
+   INT32 messageLength;
+
+   SyslogRecord(const InetAddress& a, const char *m, int len)
+   {
+      addr = a;
+      timestamp = time(NULL);
+      message = strdup(m);
+      messageLength = len;
+   }
+   ~SyslogRecord()
+   {
+      free(message);
+   }
+};
+
+/**
+ * Sender queue
+ */
+static Queue s_syslogSenderQueue;
+
+/**
+ * Shutdown trap sender
+ */
+void ShutdownSyslogSender()
+{
+   s_syslogSenderQueue.setShutdownMode();
+}
+
+/**
+ * Syslog messages sender thread
+ */
+THREAD_RESULT THREAD_CALL SyslogSender(void *)
+{
+   nxlog_debug(1, _T("Syslog sender thread started"));
+   UINT64 id = (UINT64)time(NULL) << 32;
+   while(true)
+   {
+      nxlog_debug(8, _T("SyslogSender: waiting for message"));
+      SyslogRecord *rec = (SyslogRecord *)s_syslogSenderQueue.getOrBlock();
+      if (rec == INVALID_POINTER_VALUE)
+         break;
+
+      nxlog_debug(6, _T("SyslogSender: got message from queue"));
+      bool sent = false;
+
+      NXCPMessage msg;
+      msg.setCode(CMD_SYSLOG_RECORDS);
+      msg.setId(GenerateMessageId());
+      msg.setField(VID_REQUEST_ID, id++);
+      msg.setField(VID_IP_ADDRESS, rec->addr);
+      msg.setFieldFromTime(VID_TIMESTAMP, rec->timestamp);
+      msg.setField(VID_MESSAGE, (BYTE *)rec->message, rec->messageLength + 1);
+      msg.setField(VID_MESSAGE_LENGTH, rec->messageLength);
+      msg.setField(VID_ZONE_ID, g_zoneId);
+
+      if (g_dwFlags & AF_SUBAGENT_LOADER)
+      {
+         sent = SendMessageToMasterAgent(&msg);
+      }
+      else
+      {
+         MutexLock(g_hSessionListAccess);
+         for(UINT32 i = 0; i < g_dwMaxSessions; i++)
+         {
+            if (g_pSessionList[i] != NULL)
+            {
+               if (g_pSessionList[i]->canAcceptTraps())
+               {
+                  g_pSessionList[i]->sendMessage(&msg);
+                  sent = true;
+               }
+            }
+         }
+         MutexUnlock(g_hSessionListAccess);
+      }
+
+      if (!sent)
+      {
+         nxlog_debug(6, _T("Cannot forward syslog message to server"));
+         s_syslogSenderQueue.insert(rec);
+         ThreadSleep(5);
+      }
+      else
+      {
+         nxlog_debug(6, _T("Syslog message successfully forwarded to server"));
+         delete rec;
+      }
+   }
+   nxlog_debug(1, _T("Syslog sender thread terminated"));
+   return THREAD_OK;
+}
+
+/**
+ * Syslog messages receiver thread
+ */
+THREAD_RESULT THREAD_CALL SyslogReceiver(void *)
+{
+   SOCKET hSocket = (g_dwFlags & AF_DISABLE_IPV4) ? INVALID_SOCKET : socket(AF_INET, SOCK_DGRAM, 0);
+#ifdef WITH_IPV6
+   SOCKET hSocket6 = (g_dwFlags & AF_DISABLE_IPV6) ? INVALID_SOCKET : socket(AF_INET6, SOCK_DGRAM, 0);
+#endif
+   if ((hSocket == INVALID_SOCKET)
+#ifdef WITH_IPV6
+       && (hSocket6 == INVALID_SOCKET)
+#endif
+      )
+   {
+      nxlog_debug(1, _T("SyslogReceiver: cannot create socket"));
+      return THREAD_OK;
+   }
+
+   SetSocketExclusiveAddrUse(hSocket);
+   SetSocketReuseFlag(hSocket);
+#ifndef _WIN32
+   fcntl(hSocket, F_SETFD, fcntl(hSocket, F_GETFD) | FD_CLOEXEC);
+#endif
+
+#ifdef WITH_IPV6
+   SetSocketExclusiveAddrUse(hSocket6);
+   SetSocketReuseFlag(hSocket6);
+#ifndef _WIN32
+   fcntl(hSocket6, F_SETFD, fcntl(hSocket6, F_GETFD) | FD_CLOEXEC);
+#endif
+#ifdef IPV6_V6ONLY
+   int on = 1;
+   setsockopt(hSocket6, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&on, sizeof(int));
+#endif
+#endif
+
+   // Fill in local address structure
+   struct sockaddr_in servAddr;
+   memset(&servAddr, 0, sizeof(struct sockaddr_in));
+   servAddr.sin_family = AF_INET;
+
+#ifdef WITH_IPV6
+   struct sockaddr_in6 servAddr6;
+   memset(&servAddr6, 0, sizeof(struct sockaddr_in6));
+   servAddr6.sin6_family = AF_INET6;
+#endif
+
+   if (!_tcscmp(g_szListenAddress, _T("*")))
+   {
+      servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
+#ifdef WITH_IPV6
+      memset(servAddr6.sin6_addr.s6_addr, 0, 16);
+#endif
+   }
+   else
+   {
+      InetAddress bindAddress = InetAddress::resolveHostName(g_szListenAddress, AF_INET);
+      if (bindAddress.isValid() && (bindAddress.getFamily() == AF_INET))
+      {
+         servAddr.sin_addr.s_addr = htonl(bindAddress.getAddressV4());
+      }
+      else
+      {
+         servAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+      }
+#ifdef WITH_IPV6
+      bindAddress = InetAddress::resolveHostName(g_szListenAddress, AF_INET6);
+      if (bindAddress.isValid() && (bindAddress.getFamily() == AF_INET6))
+      {
+         memcpy(servAddr6.sin6_addr.s6_addr, bindAddress.getAddressV6(), 16);
+      }
+      else
+      {
+         memset(servAddr6.sin6_addr.s6_addr, 0, 15);
+         servAddr6.sin6_addr.s6_addr[15] = 1;
+      }
+#endif
+   }
+   servAddr.sin_port = htons(g_syslogListenPort);
+#ifdef WITH_IPV6
+   servAddr6.sin6_port = htons(g_syslogListenPort);
+#endif
+
+   // Bind socket
+   TCHAR buffer[64];
+   int bindFailures = 0;
+   nxlog_debug(5, _T("SyslogReceiver: trying to bind on UDP %s:%d"), SockaddrToStr((struct sockaddr *)&servAddr, buffer), ntohs(servAddr.sin_port));
+   if (bind(hSocket, (struct sockaddr *)&servAddr, sizeof(struct sockaddr_in)) != 0)
+   {
+      nxlog_write(MSG_BIND_ERROR, EVENTLOG_ERROR_TYPE, "dse", g_syslogListenPort, _T("SyslogReceiver"), WSAGetLastError());
+      bindFailures++;
+      closesocket(hSocket);
+      hSocket = INVALID_SOCKET;
+   }
+
+#ifdef WITH_IPV6
+   nxlog_debug(5, _T("SyslogReceiver: trying to bind on UDP [%s]:%d"), SockaddrToStr((struct sockaddr *)&servAddr6, buffer), ntohs(servAddr6.sin6_port));
+   if (bind(hSocket6, (struct sockaddr *)&servAddr6, sizeof(struct sockaddr_in6)) != 0)
+   {
+      nxlog_write(MSG_BIND_ERROR, EVENTLOG_ERROR_TYPE, "dse", g_syslogListenPort, _T("SyslogReceiver"), WSAGetLastError());
+      bindFailures++;
+      closesocket(hSocket6);
+      hSocket6 = INVALID_SOCKET;
+   }
+#else
+   bindFailures++;
+#endif
+
+   // Abort if cannot bind to at least one socket
+   if (bindFailures == 2)
+   {
+      nxlog_debug(1, _T("Syslog receiver aborted - cannot bind at least one socket"));
+      return THREAD_OK;
+   }
+
+   if (hSocket != INVALID_SOCKET)
+      nxlog_debug(1, _T("Listening for syslog messages on %s:%d"), SockaddrToStr((struct sockaddr *)&servAddr, buffer), ntohs(servAddr.sin_port));
+#ifdef WITH_IPV6
+   if (hSocket6 != INVALID_SOCKET)
+      nxlog_debug(1, _T("Listening for syslog messages on [%s]:%d"), SockaddrToStr((struct sockaddr *)&servAddr6, buffer), ntohs(servAddr6.sin6_port));
+#endif
+
+   nxlog_debug(1, _T("Syslog receiver thread started"));
+
+   // Wait for packets
+   while(!(g_dwFlags & AF_SHUTDOWN))
+   {
+      struct timeval tv;
+      tv.tv_sec = 1;
+      tv.tv_usec = 0;
+
+      fd_set rdfs;
+      FD_ZERO(&rdfs);
+      if (hSocket != INVALID_SOCKET)
+         FD_SET(hSocket, &rdfs);
+#ifdef WITH_IPV6
+      if (hSocket6 != INVALID_SOCKET)
+         FD_SET(hSocket6, &rdfs);
+#endif
+
+#if defined(WITH_IPV6) && !defined(_WIN32)
+      SOCKET nfds = 0;
+      if (hSocket != INVALID_SOCKET)
+         nfds = hSocket;
+      if ((hSocket6 != INVALID_SOCKET) && (hSocket6 > nfds))
+         nfds = hSocket6;
+      int rc = select(SELECT_NFDS(nfds + 1), &rdfs, NULL, NULL, &tv);
+#else
+      int rc = select(SELECT_NFDS(hSocket + 1), &rdfs, NULL, NULL, &tv);
+#endif
+      if (rc > 0)
+      {
+         char syslogMessage[MAX_SYSLOG_MSG_LEN + 1];
+         SockAddrBuffer addr;
+         socklen_t addrLen = sizeof(SockAddrBuffer);
+#ifdef WITH_IPV6
+         SOCKET s = FD_ISSET(hSocket, &rdfs) ? hSocket : hSocket6;
+#else
+         SOCKET s = hSocket;
+#endif
+         int bytes = recvfrom(s, syslogMessage, MAX_SYSLOG_MSG_LEN, 0, (struct sockaddr *)&addr, &addrLen);
+         if (bytes > 0)
+         {
+            syslogMessage[bytes] = 0;
+            s_syslogSenderQueue.put(new SyslogRecord(InetAddress::createFromSockaddr((struct sockaddr *)&addr), syslogMessage, bytes));
+         }
+         else
+         {
+            // Sleep on error
+            ThreadSleepMs(100);
+         }
+      }
+      else if (rc == -1)
+      {
+         // Sleep on error
+         ThreadSleepMs(100);
+      }
+   }
+
+   if (hSocket != INVALID_SOCKET)
+      closesocket(hSocket);
+#ifdef WITH_IPV6
+   if (hSocket6 != INVALID_SOCKET)
+      closesocket(hSocket6);
+#endif
+
+   nxlog_debug(1, _T("Syslog receiver thread stopped"));
+   return THREAD_OK;
+}
index e070ab8..d80ad03 100644 (file)
@@ -26,6 +26,7 @@
  * Externals
  */
 void ProcessTrap(SNMP_PDU *pdu, const InetAddress& srcAddr, UINT32 zoneId, int srcPort, SNMP_Transport *pTransport, SNMP_Engine *localEngine, bool isInformRq);
+void QueueProxiedSyslogMessage(const InetAddress &addr, UINT32 zoneId, time_t timestamp, const char *msg, int msgLen);
 
 /**
  * Destructor for extended agent connection class
@@ -49,7 +50,7 @@ void AgentConnectionEx::onTrap(NXCPMessage *pMsg)
        if (m_nodeId != 0)
                pNode = (Node *)FindObjectById(m_nodeId, OBJECT_NODE);
        if (pNode == NULL)
-      pNode = FindNodeByIP(0, getIpAddr().getAddressV4());
+      pNode = FindNodeByIP(0, getIpAddr());
    if (pNode != NULL)
    {
       if (pNode->getStatus() != STATUS_UNMANAGED)
@@ -114,6 +115,46 @@ void AgentConnectionEx::onTrap(NXCPMessage *pMsg)
    }
 }
 
+/**
+ * Incoming syslog message processor
+ */
+void AgentConnectionEx::onSyslogMessage(NXCPMessage *msg)
+{
+   TCHAR buffer[64];
+   nxlog_debug(3, _T("AgentConnectionEx::onSyslogMessage(): Received message from agent at %s, node ID %d"), getIpAddr().toString(buffer), m_nodeId);
+
+   UINT32 zoneId = msg->getFieldAsUInt32(VID_ZONE_ID);
+   Node *node = NULL;
+   if (m_nodeId != 0)
+      node = (Node *)FindObjectById(m_nodeId, OBJECT_NODE);
+   if (node == NULL)
+      node = FindNodeByIP(zoneId, getIpAddr());
+   if (node != NULL)
+   {
+      // Check for duplicate messages - only accept messages with ID
+      // higher than last received
+      if (node->checkSyslogMessageId(msg->getFieldAsUInt64(VID_REQUEST_ID)))
+      {
+         int msgLen = msg->getFieldAsInt32(VID_MESSAGE_LENGTH);
+         if (msgLen < 2048)
+         {
+            char message[2048];
+            msg->getFieldAsBinary(VID_MESSAGE, (BYTE *)message, msgLen + 1);
+            QueueProxiedSyslogMessage(msg->getFieldAsInetAddress(VID_IP_ADDRESS), msg->getFieldAsUInt32(VID_ZONE_ID),
+                                      msg->getFieldAsTime(VID_TIMESTAMP), message, msgLen);
+         }
+      }
+      else
+      {
+         nxlog_debug(5, _T("AgentConnectionEx::onSyslogMessage(): message ID is invalid (node %s [%d])"), node->getName(), node->getId());
+      }
+   }
+   else
+   {
+      nxlog_debug(5, _T("AgentConnectionEx::onSyslogMessage(): Cannot find node for IP address %s"), getIpAddr().toString(buffer));
+   }
+}
+
 /**
  * Handler for data push
  */
index e4ddfbe..8ca2bca 100644 (file)
@@ -830,8 +830,7 @@ retry_db_lock:
                ThreadCreate(SNMPTrapReceiver, 0, NULL);
 
        // Start built-in syslog daemon
-       if (ConfigReadInt(_T("EnableSyslogDaemon"), 0))
-          StartSyslogServer();
+   StartSyslogServer();
 
        // Start database _T("lazy") write thread
        StartDBWriter();
index d88fb32..a2224c2 100644 (file)
@@ -68,6 +68,7 @@ Node::Node() : DataCollectionTarget()
    m_smclpConnection = NULL;
        m_lastAgentTrapId = 0;
        m_lastSNMPTrapId = 0;
+       m_lastSyslogMessageId = 0;
    m_lastAgentPushRequestId = 0;
    m_szAgentVersion[0] = 0;
    m_szPlatformName[0] = 0;
@@ -164,6 +165,7 @@ Node::Node(const InetAddress& addr, UINT32 dwFlags, UINT32 agentProxy, UINT32 sn
    m_smclpConnection = NULL;
        m_lastAgentTrapId = 0;
        m_lastSNMPTrapId = 0;
+       m_lastSyslogMessageId = 0;
    m_lastAgentPushRequestId = 0;
    m_szAgentVersion[0] = 0;
    m_szPlatformName[0] = 0;
@@ -7195,16 +7197,29 @@ bool Node::checkAgentTrapId(UINT64 trapId)
 /**
  * Check and update last agent SNMP trap ID
  */
-bool Node::checkSNMPTrapId(UINT32 trapId)
+bool Node::checkSNMPTrapId(UINT32 id)
 {
        lockProperties();
-       bool valid = (trapId > m_lastSNMPTrapId);
+       bool valid = (id > m_lastSNMPTrapId);
        if (valid)
-               m_lastSNMPTrapId = trapId;
+               m_lastSNMPTrapId = id;
        unlockProperties();
        return valid;
 }
 
+/**
+ * Check and update last syslog message ID
+ */
+bool Node::checkSyslogMessageId(UINT64 id)
+{
+   lockProperties();
+   bool valid = (id > m_lastSyslogMessageId);
+   if (valid)
+      m_lastSyslogMessageId = id;
+   unlockProperties();
+   return valid;
+}
+
 /**
  * Check and update last agent data push request ID
  */
index cc0a3f9..e1e96fd 100644 (file)
@@ -36,13 +36,25 @@ class QueuedSyslogMessage
 {
 public:
    InetAddress sourceAddr;
+   time_t timestamp;
+   UINT32 zoneId;
    char *message;
    int messageLength;
 
-   QueuedSyslogMessage(const InetAddress& addr, char *msg, int msgLen) : sourceAddr(addr)
+   QueuedSyslogMessage(const InetAddress& addr, const char *msg, int msgLen) : sourceAddr(addr)
    {
       message = (char *)nx_memdup(msg, msgLen + 1);
       messageLength = msgLen;
+      timestamp = time(NULL);
+      zoneId = 0;
+   }
+
+   QueuedSyslogMessage(const InetAddress& addr, time_t t, UINT32 zid, const char *msg, int msgLen) : sourceAddr(addr)
+   {
+      message = (char *)nx_memdup(msg, msgLen + 1);
+      messageLength = msgLen;
+      timestamp = t;
+      zoneId = zid;
    }
 
    ~QueuedSyslogMessage()
@@ -79,6 +91,8 @@ static LogParser *s_parser = NULL;
 static MUTEX s_parserLock = INVALID_MUTEX_HANDLE;
 static NodeMatchingPolicy s_nodeMatchingPolicy = SOURCE_IP_THEN_HOSTNAME;
 static THREAD s_receiverThread = INVALID_THREAD_HANDLE;
+static THREAD s_processingThread = INVALID_THREAD_HANDLE;
+static THREAD s_writerThread = INVALID_THREAD_HANDLE;
 static bool s_running = true;
 static bool s_alwaysUseServerTime = false;
 
@@ -176,7 +190,7 @@ static BOOL ParseTimeStamp(char **ppStart, int nMsgSize, int *pnPos, time_t *ptm
 /**
  * Parse syslog message
  */
-static BOOL ParseSyslogMessage(char *psMsg, int nMsgLen, NX_SYSLOG_RECORD *pRec)
+static BOOL ParseSyslogMessage(char *psMsg, int nMsgLen, time_t receiverTime, NX_SYSLOG_RECORD *pRec)
 {
    int i, nLen, nPos = 0;
    char *pCurr = psMsg;
@@ -219,7 +233,7 @@ static BOOL ParseSyslogMessage(char *psMsg, int nMsgLen, NX_SYSLOG_RECORD *pRec)
       // We still had to parse timestamp to get correct start position for MSG part
       if (s_alwaysUseServerTime)
       {
-         pRec->tmTimeStamp = time(NULL);
+         pRec->tmTimeStamp = receiverTime;
       }
 
       // Hostname
@@ -240,7 +254,7 @@ static BOOL ParseSyslogMessage(char *psMsg, int nMsgLen, NX_SYSLOG_RECORD *pRec)
    }
    else
    {
-      pRec->tmTimeStamp = time(NULL);
+      pRec->tmTimeStamp = receiverTime;
    }
 
    // Parse MSG part
@@ -262,7 +276,7 @@ static BOOL ParseSyslogMessage(char *psMsg, int nMsgLen, NX_SYSLOG_RECORD *pRec)
 /**
  * Find node by host name
  */
-static Node *FindNodeByHostname(const char *hostName)
+static Node *FindNodeByHostname(const char *hostName, UINT32 zoneId)
 {
    if (hostName[0] == 0)
       return NULL;
@@ -271,7 +285,7 @@ static Node *FindNodeByHostname(const char *hostName)
    InetAddress ipAddr = InetAddress::resolveHostName(hostName);
        if (ipAddr.isValidUnicast())
    {
-      node = FindNodeByIP((g_flags & AF_TRAP_SOURCES_IN_ALL_ZONES) ? ALL_ZONES : 0, ipAddr);
+      node = FindNodeByIP((g_flags & AF_TRAP_SOURCES_IN_ALL_ZONES) ? ALL_ZONES : zoneId, ipAddr);
    }
 
    if (node == NULL)
@@ -292,24 +306,24 @@ static Node *FindNodeByHostname(const char *hostName)
  * Bind syslog message to NetXMS node object
  * sourceAddr is an IP address from which we receive message
  */
-static Node *BindMsgToNode(NX_SYSLOG_RECORD *pRec, const InetAddress& sourceAddr)
+static Node *BindMsgToNode(NX_SYSLOG_RECORD *pRec, const InetAddress& sourceAddr, UINT32 zoneId)
 {
    Node *node = NULL;
 
    if (s_nodeMatchingPolicy == SOURCE_IP_THEN_HOSTNAME)
    {
-      node = FindNodeByIP((g_flags & AF_TRAP_SOURCES_IN_ALL_ZONES) ? ALL_ZONES : 0, sourceAddr);
+      node = FindNodeByIP((g_flags & AF_TRAP_SOURCES_IN_ALL_ZONES) ? ALL_ZONES : zoneId, sourceAddr);
       if (node == NULL)
       {
-         node = FindNodeByHostname(pRec->szHostName);
+         node = FindNodeByHostname(pRec->szHostName, zoneId);
       }
    }
    else
    {
-      node = FindNodeByHostname(pRec->szHostName);
+      node = FindNodeByHostname(pRec->szHostName, zoneId);
       if (node == NULL)
       {
-         node = FindNodeByIP((g_flags & AF_TRAP_SOURCES_IN_ALL_ZONES) ? ALL_ZONES : 0, sourceAddr);
+         node = FindNodeByIP((g_flags & AF_TRAP_SOURCES_IN_ALL_ZONES) ? ALL_ZONES : zoneId, sourceAddr);
       }
    }
 
@@ -414,17 +428,17 @@ static THREAD_RESULT THREAD_CALL SyslogWriterThread(void *arg)
 /**
  * Process syslog message
  */
-static void ProcessSyslogMessage(char *psMsg, int nMsgLen, const InetAddress& sourceAddr)
+static void ProcessSyslogMessage(QueuedSyslogMessage *msg)
 {
    NX_SYSLOG_RECORD record;
 
-       DbgPrintf(6, _T("ProcessSyslogMessage: Raw syslog message to process:\n%hs"), psMsg);
-   if (ParseSyslogMessage(psMsg, nMsgLen, &record))
+       DbgPrintf(6, _T("ProcessSyslogMessage: Raw syslog message to process:\n%hs"), msg->message);
+   if (ParseSyslogMessage(msg->message, msg->messageLength, msg->timestamp, &record))
    {
       g_syslogMessagesReceived++;
 
       record.qwMsgId = s_msgId++;
-      Node *node = BindMsgToNode(&record, sourceAddr);
+      Node *node = BindMsgToNode(&record, msg->sourceAddr, msg->zoneId);
 
       g_syslogWriteQueue.put(nx_memdup(&record, sizeof(NX_SYSLOG_RECORD)));
 
@@ -433,7 +447,7 @@ static void ProcessSyslogMessage(char *psMsg, int nMsgLen, const InetAddress& so
 
                TCHAR ipAddr[64];
                DbgPrintf(6, _T("Syslog message: ipAddr=%s objectId=%d tag=\"%hs\" msg=\"%hs\""),
-                         sourceAddr.toString(ipAddr), record.dwSourceObject, record.szTag, record.szMessage);
+                         msg->sourceAddr.toString(ipAddr), record.dwSourceObject, record.szTag, record.szMessage);
 
                MutexLock(s_parserLock);
                if ((record.dwSourceObject != 0) && (s_parser != NULL) &&
@@ -470,7 +484,7 @@ static THREAD_RESULT THREAD_CALL SyslogProcessingThread(void *pArg)
       if (msg == INVALID_POINTER_VALUE)
          break;
 
-      ProcessSyslogMessage(msg->message, msg->messageLength, msg->sourceAddr);
+      ProcessSyslogMessage(msg);
       delete msg;
    }
    return THREAD_OK;
@@ -484,6 +498,14 @@ static void QueueSyslogMessage(char *msg, int msgLen, const InetAddress& sourceA
    g_syslogProcessingQueue.put(new QueuedSyslogMessage(sourceAddr, msg, msgLen));
 }
 
+/**
+ * Queue proxied syslog message for processing
+ */
+void QueueProxiedSyslogMessage(const InetAddress &addr, UINT32 zoneId, time_t timestamp, const char *msg, int msgLen)
+{
+   g_syslogProcessingQueue.put(new QueuedSyslogMessage(addr, timestamp, zoneId, msg, msgLen));
+}
+
 /**
  * Callback for syslog parser
  */
@@ -703,17 +725,6 @@ static THREAD_RESULT THREAD_CALL SyslogReceiver(void *pArg)
       nxlog_write(MSG_LISTENING_FOR_SYSLOG, EVENTLOG_INFORMATION_TYPE, "Hd", servAddr6.sin6_addr.s6_addr, port);
 #endif
 
-   SetLogParserTraceCallback(nxlog_debug2);
-   InitLogParserLibrary();
-
-       // Create message parser
-       s_parserLock = MutexCreate();
-       CreateParserFromConfig();
-
-   // Start processing thread
-   THREAD hProcessingThread = ThreadCreateEx(SyslogProcessingThread, 0, NULL);
-   THREAD hWriterThread = ThreadCreateEx(SyslogWriterThread, 0, NULL);
-
    DbgPrintf(1, _T("Syslog receiver thread started"));
 
    // Wait for packets
@@ -771,54 +782,17 @@ static THREAD_RESULT THREAD_CALL SyslogReceiver(void *pArg)
       }
    }
 
-   // Stop processing thread
-   g_syslogProcessingQueue.put(INVALID_POINTER_VALUE);
-   ThreadJoin(hProcessingThread);
-
-   // Stop writer thread - it must be done after processing thread already finished
-   g_syslogWriteQueue.put(INVALID_POINTER_VALUE);
-   ThreadJoin(hWriterThread);
-
-       delete s_parser;
-   CleanupLogParserLibrary();
+   if (hSocket != INVALID_SOCKET)
+      closesocket(hSocket);
+#ifdef WITH_IPV6
+   if (hSocket6 != INVALID_SOCKET)
+      closesocket(hSocket6);
+#endif
 
-   DbgPrintf(1, _T("Syslog receiver thread stopped"));
+   nxlog_debug(1, _T("Syslog receiver thread stopped"));
    return THREAD_OK;
 }
 
-/**
- * Start built-in syslog server
- */
-void StartSyslogServer()
-{
-   s_nodeMatchingPolicy = (NodeMatchingPolicy)ConfigReadInt(_T("SyslogNodeMatchingPolicy"), SOURCE_IP_THEN_HOSTNAME);
-   s_alwaysUseServerTime = ConfigReadInt(_T("SyslogIgnoreMessageTimestamp"), 0) ? true : false;
-
-   // Determine first available message id
-   DB_HANDLE hdb = DBConnectionPoolAcquireConnection();
-   DB_RESULT hResult = DBSelect(hdb, _T("SELECT max(msg_id) FROM syslog"));
-   if (hResult != NULL)
-   {
-      if (DBGetNumRows(hResult) > 0)
-      {
-         s_msgId = max(DBGetFieldUInt64(hResult, 0, 0) + 1, s_msgId);
-      }
-      DBFreeResult(hResult);
-   }
-   DBConnectionPoolReleaseConnection(hdb);
-
-   s_receiverThread = ThreadCreateEx(SyslogReceiver, 0, NULL);
-}
-
-/**
- * Stop built-in syslog server
- */
-void StopSyslogServer()
-{
-   s_running = false;
-   ThreadJoin(s_receiverThread);
-}
-
 /**
  * Create NXCP message from NX_SYSLOG_RECORD structure
  */
@@ -834,7 +808,7 @@ void CreateMessageFromSyslogMsg(NXCPMessage *pMsg, NX_SYSLOG_RECORD *pRec)
    pMsg->setField(dwId++, pRec->dwSourceObject);
    pMsg->setFieldFromMBString(dwId++, pRec->szHostName);
    pMsg->setFieldFromMBString(dwId++, pRec->szTag);
-       pMsg->setFieldFromMBString(dwId++, pRec->szMessage);
+   pMsg->setFieldFromMBString(dwId++, pRec->szMessage);
 }
 
 /**
@@ -842,9 +816,9 @@ void CreateMessageFromSyslogMsg(NXCPMessage *pMsg, NX_SYSLOG_RECORD *pRec)
  */
 void ReinitializeSyslogParser()
 {
-       if (s_parserLock == INVALID_MUTEX_HANDLE)
-               return; // Syslog daemon not initialized
-       CreateParserFromConfig();
+   if (s_parserLock == INVALID_MUTEX_HANDLE)
+      return;  // Syslog daemon not initialized
+   CreateParserFromConfig();
 }
 
 /**
@@ -858,3 +832,59 @@ void OnSyslogConfigurationChange(const TCHAR *name, const TCHAR *value)
       nxlog_debug(4, _T("Syslog: ignore message timestamp option set to %s"), s_alwaysUseServerTime ? _T("ON") : _T("OFF"));
    }
 }
+
+/**
+ * Start built-in syslog server
+ */
+void StartSyslogServer()
+{
+   s_nodeMatchingPolicy = (NodeMatchingPolicy)ConfigReadInt(_T("SyslogNodeMatchingPolicy"), SOURCE_IP_THEN_HOSTNAME);
+   s_alwaysUseServerTime = ConfigReadInt(_T("SyslogIgnoreMessageTimestamp"), 0) ? true : false;
+
+   // Determine first available message id
+   DB_HANDLE hdb = DBConnectionPoolAcquireConnection();
+   DB_RESULT hResult = DBSelect(hdb, _T("SELECT max(msg_id) FROM syslog"));
+   if (hResult != NULL)
+   {
+      if (DBGetNumRows(hResult) > 0)
+      {
+         s_msgId = max(DBGetFieldUInt64(hResult, 0, 0) + 1, s_msgId);
+      }
+      DBFreeResult(hResult);
+   }
+   DBConnectionPoolReleaseConnection(hdb);
+
+   SetLogParserTraceCallback(nxlog_debug2);
+   InitLogParserLibrary();
+
+   // Create message parser
+   s_parserLock = MutexCreate();
+   CreateParserFromConfig();
+
+   // Start processing thread
+   s_processingThread = ThreadCreateEx(SyslogProcessingThread, 0, NULL);
+   s_writerThread = ThreadCreateEx(SyslogWriterThread, 0, NULL);
+
+   if (ConfigReadInt(_T("EnableSyslogReceiver"), 0))
+      s_receiverThread = ThreadCreateEx(SyslogReceiver, 0, NULL);
+}
+
+/**
+ * Stop built-in syslog server
+ */
+void StopSyslogServer()
+{
+   s_running = false;
+   ThreadJoin(s_receiverThread);
+
+   // Stop processing thread
+   g_syslogProcessingQueue.put(INVALID_POINTER_VALUE);
+   ThreadJoin(s_processingThread);
+
+   // Stop writer thread - it must be done after processing thread already finished
+   g_syslogWriteQueue.put(INVALID_POINTER_VALUE);
+   ThreadJoin(s_writerThread);
+
+   delete s_parser;
+   CleanupLogParserLibrary();
+}
index 6e8e58c..bcf15e6 100644 (file)
@@ -127,6 +127,7 @@ protected:
 
    virtual void printMsg(const TCHAR *format, ...);
    virtual void onTrap(NXCPMessage *msg);
+   virtual void onSyslogMessage(NXCPMessage *pMsg);
    virtual void onDataPush(NXCPMessage *msg);
    virtual void onFileMonitoringData(NXCPMessage *msg);
    virtual void onSnmpTrap(NXCPMessage *pMsg);
@@ -1343,9 +1344,10 @@ protected:
    AgentConnectionEx *m_agentConnection;
    AgentConnectionEx *m_snmpProxyConnection;
    SMCLP_Connection *m_smclpConnection;
-       QWORD m_lastAgentTrapId;             // ID of last received agent trap
-   QWORD m_lastAgentPushRequestId; // ID of last received agent push request
+       UINT64 m_lastAgentTrapId;            // ID of last received agent trap
+   UINT64 m_lastAgentPushRequestId; // ID of last received agent push request
    UINT32 m_lastSNMPTrapId;
+   UINT64 m_lastSyslogMessageId; // ID of last received syslog message
    UINT32 m_pollerNode;      // Node used for network service polling
    UINT32 m_agentProxy;      // Node used as proxy for agent connection
        UINT32 m_snmpProxy;       // Node used as proxy for SNMP requests
@@ -1589,6 +1591,7 @@ public:
 
        bool checkAgentTrapId(UINT64 id);
        bool checkSNMPTrapId(UINT32 id);
+   bool checkSyslogMessageId(UINT64 id);
    bool checkAgentPushRequestId(UINT64 id);
 
    bool connectToSMCLP();
index 75cea86..df2c317 100644 (file)
@@ -498,6 +498,7 @@ private:
    void onDataPushCallback(NXCPMessage *msg);
    void onSnmpTrapCallback(NXCPMessage *msg);
    void onTrapCallback(NXCPMessage *msg);
+   void onSyslogMessageCallback(NXCPMessage *msg);
 
 protected:
    void destroyResultData();
@@ -510,6 +511,7 @@ protected:
 
    virtual void printMsg(const TCHAR *format, ...);
    virtual void onTrap(NXCPMessage *pMsg);
+   virtual void onSyslogMessage(NXCPMessage *pMsg);
        virtual void onDataPush(NXCPMessage *msg);
        virtual void onFileMonitoringData(NXCPMessage *msg);
        virtual void onSnmpTrap(NXCPMessage *pMsg);
index 1cf02f6..1272372 100644 (file)
@@ -350,6 +350,17 @@ void AgentConnection::receiverThread()
                   delete pMsg;
                }
                                        break;
+            case CMD_SYSLOG_RECORDS:
+               if (g_agentConnectionThreadPool != NULL)
+               {
+                  incInternalRefCount();
+                  ThreadPoolExecute(g_agentConnectionThreadPool, this, &AgentConnection::onSyslogMessageCallback, pMsg);
+               }
+               else
+               {
+                  delete pMsg;
+               }
+               break;
                                case CMD_PUSH_DCI_DATA:
                                   if (g_agentConnectionThreadPool != NULL)
                                   {
@@ -999,7 +1010,7 @@ bool AgentConnection::sendRawMessage(NXCP_MESSAGE *pMsg)
 }
 
 /**
- * Callback for processing data push on separate thread
+ * Callback for processing incoming event on separate thread
  */
 void AgentConnection::onTrapCallback(NXCPMessage *msg)
 {
@@ -1016,6 +1027,24 @@ void AgentConnection::onTrap(NXCPMessage *pMsg)
 {
 }
 
+/**
+ * Callback for processing incoming syslog message on separate thread
+ */
+void AgentConnection::onSyslogMessageCallback(NXCPMessage *msg)
+{
+   onSyslogMessage(msg);
+   delete msg;
+   decInternalRefCount();
+}
+
+/**
+ * Syslog message handler. Should be overriden in derived classes to implement
+ * actual message processing. Default implementation do nothing.
+ */
+void AgentConnection::onSyslogMessage(NXCPMessage *pMsg)
+{
+}
+
 /**
  * Callback for processing data push on separate thread
  */
index e6d79ef..b6569cf 100644 (file)
@@ -721,6 +721,16 @@ static bool SetSchemaVersion(int version)
    return SQLQuery(query);
 }
 
+/**
+ * Upgrade from V413 to V414
+ */
+static BOOL H_UpgradeFromV413(int currVersion, int newVersion)
+{
+   CHK_EXEC(SQLQuery(_T("UPDATE config SET var_name='EnableSyslogReceiver' WHERE var_name='EnableSyslogDaemon'")));
+   CHK_EXEC(SetSchemaVersion(414));
+   return TRUE;
+}
+
 /**
  * Upgrade from V412 to V413
  */
@@ -10536,6 +10546,7 @@ static struct
    { 410, 411, H_UpgradeFromV410 },
    { 411, 412, H_UpgradeFromV411 },
    { 412, 413, H_UpgradeFromV412 },
+   { 413, 414, H_UpgradeFromV413 },
    { 0, 0, NULL }
 };