Browse Source
Merge pull request #1280 from etobella/11.0-mig-nsca_client
Merge pull request #1280 from etobella/11.0-mig-nsca_client
[11.0] nsca clientpull/1414/head
Jordi Ballester Alomar
6 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1213 additions and 0 deletions
-
1.travis.yml
-
126nsca_client/README.rst
-
4nsca_client/__init__.py
-
22nsca_client/__manifest__.py
-
22nsca_client/demo/demo_data.xml
-
225nsca_client/i18n/fr.po
-
192nsca_client/i18n/nsca_client.pot
-
1nsca_client/models/__init__.py
-
108nsca_client/models/nsca_check.py
-
239nsca_client/models/nsca_server.py
-
3nsca_client/security/ir.model.access.csv
-
BINnsca_client/static/description/check.png
-
BINnsca_client/static/description/icon.png
-
BINnsca_client/static/description/server.png
-
1nsca_client/tests/__init__.py
-
104nsca_client/tests/test_nsca.py
-
90nsca_client/views/nsca_check.xml
-
12nsca_client/views/nsca_menu.xml
-
63nsca_client/views/nsca_server.xml
@ -0,0 +1,126 @@ |
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
|||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
|||
:alt: License: AGPL-3 |
|||
|
|||
=========== |
|||
NSCA Client |
|||
=========== |
|||
|
|||
This is a technical module to send passive alerts to your favorite NSCA daemon |
|||
(Nagios, Shinken...). |
|||
This module is based on the Odoo cron system and requires a NSCA client |
|||
installed on the system to satisfy the ``/usr/sbin/send_nsca`` command. |
|||
|
|||
Installation |
|||
============ |
|||
|
|||
To use this module, you need to install a NSCA client. |
|||
|
|||
On Debian/Ubuntu:: |
|||
|
|||
$ sudo apt-get install nsca-client |
|||
|
|||
Configuration |
|||
============= |
|||
|
|||
To configure this module, you need to: |
|||
|
|||
* Configure your server and a passive service in your monitoring tool |
|||
(e.g service ``Odoo Mail Queue`` on host ``MY-SERVER``). |
|||
|
|||
* Declare your NSCA server in the menu Configuration / Technical / NSCA Client / Servers |
|||
|
|||
.. image:: nsca_client/static/description/server.png |
|||
:width: 400 px |
|||
|
|||
* Create NSCA checks in the menu Configuration / Technical / NSCA Client / Checks |
|||
|
|||
.. image:: nsca_client/static/description/check.png |
|||
:width: 400 px |
|||
|
|||
* Code the methods which will be called by the NSCA checks. |
|||
|
|||
Such methods must return a tuple ``(RC, MESSAGE, PERFORMANCE_DATA)`` where ``RC`` is an integer, |
|||
``MESSAGE`` a unicode string AND ``PERFOMANCE_DATA`` is a dictionary. |
|||
``RC`` values and the corresponding status are: |
|||
|
|||
- 0: OK |
|||
- 1: WARNING |
|||
- 2: CRITICAL |
|||
- 3: UNKNOWN |
|||
|
|||
``PERFORMANCE_DATA`` is not mandatory, so it could be possible to send |
|||
``(RC, MESSAGE)``. |
|||
Each element of ``PERFORMANCE_DATA`` will be a dictionary that could contain: |
|||
|
|||
- value: value of the data (required) |
|||
- max: Max value on the chart |
|||
- min: Minimum value on the chart |
|||
- warn: Warning value on the chart |
|||
- crit: Critical value on the chart |
|||
- uom: Unit of Measure on the chart (s - Seconds, % - Percentage, B - Bytes, c - Continuous) |
|||
|
|||
The key of the dictionary will be used as the performance_data label. |
|||
|
|||
E.g: |
|||
|
|||
.. code-block:: python |
|||
|
|||
class MailMail(models.Model): |
|||
_inherit = 'mail.mail' |
|||
|
|||
@api.model |
|||
def nsca_check_mails(self): |
|||
mails = self.search([('state', '=', 'exception')]) |
|||
if mails: |
|||
return (1, u"%s mails not sent" % len(mails), { |
|||
'exceptions': {'value': len(mails)}}) |
|||
return (0, u"OK", {'exceptions': {'value': len(mails)}}) |
|||
|
|||
On the example, the performance data will use the label ``exceptions`` and the |
|||
value will be the number of exception of mails. |
|||
|
|||
Usage |
|||
===== |
|||
|
|||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas |
|||
:alt: Try me on Runbot |
|||
:target: https://runbot.odoo-community.org/runbot/149/11.0 |
|||
|
|||
Bug Tracker |
|||
=========== |
|||
|
|||
Bugs are tracked on `GitHub Issues |
|||
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please |
|||
check there if your issue has already been reported. If you spotted it first, |
|||
help us smashing it by providing a detailed and welcomed feedback. |
|||
|
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Images |
|||
------ |
|||
|
|||
* Daniel Foré: `Icon <http://www.iconarchive.com/show/elementary-icons-by-danrabbit/Apps-system-monitor-icon.html>`_ (Elementary theme, GPL). |
|||
|
|||
Contributors |
|||
------------ |
|||
|
|||
* Sébastien Alix <sebastien.alix@osiell.com> |
|||
* Enric Tobella <etobella@creublanca.es> |
|||
|
|||
Maintainer |
|||
---------- |
|||
|
|||
.. image:: https://odoo-community.org/logo.png |
|||
:alt: Odoo Community Association |
|||
:target: https://odoo-community.org |
|||
|
|||
This module is maintained by the OCA. |
|||
|
|||
OCA, or the Odoo Community Association, is a nonprofit organization whose |
|||
mission is to support the collaborative development of Odoo features and |
|||
promote its widespread use. |
|||
|
|||
To contribute to this module, please visit https://odoo-community.org. |
@ -0,0 +1,4 @@ |
|||
# © 2015 ABF OSIELL <http://osiell.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
|
|||
from . import models |
@ -0,0 +1,22 @@ |
|||
# © 2015 ABF OSIELL <http://osiell.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
{ |
|||
"name": "NSCA Client", |
|||
"summary": "Send passive alerts to monitor your Odoo application.", |
|||
"version": "11.0.1.0.0", |
|||
"category": "Tools", |
|||
"website": "http://github.com/OCA/server-tools", |
|||
"author": "ABF OSIELL, Odoo Community Association (OCA)", |
|||
"license": "AGPL-3", |
|||
"application": False, |
|||
"installable": True, |
|||
"data": [ |
|||
"security/ir.model.access.csv", |
|||
"views/nsca_menu.xml", |
|||
"views/nsca_check.xml", |
|||
"views/nsca_server.xml", |
|||
], |
|||
"demo": [ |
|||
"demo/demo_data.xml", |
|||
], |
|||
} |
@ -0,0 +1,22 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<data noupdate="1"> |
|||
|
|||
<record id="nsca_server_default" model="nsca.server"> |
|||
<field name="name">nagios.example.net</field> |
|||
<field name="port">5667</field> |
|||
<field name="node_hostname">MY-SERVER</field> |
|||
</record> |
|||
|
|||
<record id="demo_nsca_check_mails" model="nsca.check"> |
|||
<field name="server_id" ref="nsca_server_default"/> |
|||
<field name="service">Odoo Mail Queue</field> |
|||
<field name="interval_number" eval="10"/> |
|||
<field name="interval_type">minutes</field> |
|||
<field name="nsca_model">mail.mail</field> |
|||
<field name="nsca_function">nsca_check_mails</field> |
|||
<field name="nsca_args"></field> |
|||
</record> |
|||
|
|||
</data> |
|||
</odoo> |
@ -0,0 +1,225 @@ |
|||
# Translation of Odoo Server. |
|||
# This file contains the translation of the following modules: |
|||
# * nsca_client |
|||
# |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: Odoo Server 8.0\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"POT-Creation-Date: 2016-04-07 12:33+0000\n" |
|||
"PO-Revision-Date: 2016-04-07 12:33+0000\n" |
|||
"Last-Translator: <>\n" |
|||
"Language-Team: \n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: \n" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "(1, u\"3 mails not sent\")" |
|||
msgstr "(1, u\"3 mails non-envoyés\")" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "0: OK" |
|||
msgstr "0: OK" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "1: WARNING" |
|||
msgstr "1: WARNING" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "2: CRITICAL" |
|||
msgstr "2: CRITICAL" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "3: UNKNOWN" |
|||
msgstr "3: UNKNOWN" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,allow_void_result:0 |
|||
msgid "Allow void result" |
|||
msgstr "Autoriser l'absence de résultat" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "Any other RC value will be treated as CRITICAL." |
|||
msgstr "Tout autre valeur RC sera traitée comme CRITICAL." |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,nsca_args:0 |
|||
msgid "Arguments" |
|||
msgstr "Arguments" |
|||
|
|||
#. module: nsca_client |
|||
#: help:nsca.check,allow_void_result:0 |
|||
msgid "By default, a CRITICAL message is sent if the method does not return.\n" |
|||
"If checked, no message will be sent in such a case." |
|||
msgstr "Par défaut, un message de niveau CRITICAL sera envoyé si la méthode ne retourne aucun résultat.\n" |
|||
"Si cette option est cochée, aucun message ne sera envoyé dans un tel cas." |
|||
|
|||
#. module: nsca_client |
|||
#: model:ir.actions.act_window,name:nsca_client.action_nsca_check_tree |
|||
#: model:ir.ui.menu,name:nsca_client.menu_action_nsca_check_tree |
|||
#: view:nsca.server:nsca_client.view_nsca_server_form |
|||
#: field:nsca.server,check_ids:0 |
|||
msgid "Checks" |
|||
msgstr "Contrôles" |
|||
|
|||
#. module: nsca_client |
|||
#: code:addons/nsca_client/models/nsca_check.py:154 |
|||
#, python-format |
|||
msgid "Command '%s' not found. Please install the NSCA client.\n" |
|||
"On Debian/Ubuntu: apt-get install nsca-client" |
|||
msgstr "Commande '%s' non-disponible. Veuillez installer le client NSCA.\n" |
|||
"Sur Debian/Ubuntu: apt-get install nsca-client" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,create_uid:0 |
|||
#: field:nsca.server,create_uid:0 |
|||
msgid "Created by" |
|||
msgstr "Créé par" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,create_date:0 |
|||
#: field:nsca.server,create_date:0 |
|||
msgid "Created on" |
|||
msgstr "Créé le" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,cron_id:0 |
|||
msgid "Cron" |
|||
msgstr "Cron" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "E.g." |
|||
msgstr "Ex :" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.server,encryption_method:0 |
|||
msgid "Encryption method" |
|||
msgstr "Méthode de chiffrement" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "Frequency" |
|||
msgstr "Fréquence" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.server,name:0 |
|||
msgid "Hostname" |
|||
msgstr "Serveur" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.server,node_hostname:0 |
|||
msgid "Hostname of this node" |
|||
msgstr "Nom d'hôte du noeud" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,id:0 |
|||
#: field:nsca.server,id:0 |
|||
msgid "ID" |
|||
msgstr "ID" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,write_uid:0 |
|||
#: field:nsca.server,write_uid:0 |
|||
msgid "Last Updated by" |
|||
msgstr "Dernière modification par" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,write_date:0 |
|||
#: field:nsca.server,write_date:0 |
|||
msgid "Last Updated on" |
|||
msgstr "Dernière mise à jour le" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,nsca_function:0 |
|||
msgid "Method" |
|||
msgstr "Méthode" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,nsca_model:0 |
|||
msgid "Model" |
|||
msgstr "Modèle" |
|||
|
|||
#. module: nsca_client |
|||
#: code:addons/nsca_client/models/nsca_check.py:64 |
|||
#: model:ir.model,name:nsca_client.model_nsca_check |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
#, python-format |
|||
msgid "NSCA Check" |
|||
msgstr "Contrôle NSCA" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_tree |
|||
msgid "NSCA Checks" |
|||
msgstr "Contrôles NSCA" |
|||
|
|||
#. module: nsca_client |
|||
#: model:ir.ui.menu,name:nsca_client.menu_nsca_client |
|||
msgid "NSCA Client" |
|||
msgstr "Client NSCA" |
|||
|
|||
#. module: nsca_client |
|||
#: model:ir.model,name:nsca_client.model_nsca_server |
|||
#: view:nsca.server:nsca_client.view_nsca_server_form |
|||
msgid "NSCA Server" |
|||
msgstr "Serveur NSCA" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.server:nsca_client.view_nsca_server_tree |
|||
msgid "NSCA Servers" |
|||
msgstr "Serveurs NSCA" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.server:nsca_client.view_nsca_server_form |
|||
msgid "Node identity" |
|||
msgstr "Identité du noeud" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.server,password:0 |
|||
msgid "Password" |
|||
msgstr "Mot de passe" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.server,port:0 |
|||
msgid "Port" |
|||
msgstr "Port" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,server_id:0 |
|||
msgid "Server" |
|||
msgstr "Serveur" |
|||
|
|||
#. module: nsca_client |
|||
#: model:ir.actions.act_window,name:nsca_client.action_nsca_server_tree |
|||
#: model:ir.ui.menu,name:nsca_client.menu_action_nsca_server_tree |
|||
msgid "Servers" |
|||
msgstr "Serveurs" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,service:0 |
|||
msgid "Service" |
|||
msgstr "Service" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "Settings" |
|||
msgstr "Paramètres" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "The method must return a tuple (RC, MESSAGE) where RC is an integer:" |
|||
msgstr "La méthode doit retourner un tuple (RC, MESSAGE) où RC est un entier :" |
|||
|
|||
#. module: nsca_client |
|||
#: help:nsca.server,node_hostname:0 |
|||
msgid "This is the hostname of the current Odoo node declared in the monitoring server." |
|||
msgstr "C'est le nom d'hôte identifiant le serveur Odoo dans la configuration du serveur NSCA." |
|||
|
@ -0,0 +1,192 @@ |
|||
# Translation of Odoo Server. |
|||
# This file contains the translation of the following modules: |
|||
# * nsca_client |
|||
# |
|||
msgid "" |
|||
msgstr "" |
|||
"Project-Id-Version: Odoo Server 8.0\n" |
|||
"Report-Msgid-Bugs-To: \n" |
|||
"POT-Creation-Date: 2015-12-17 07:05+0000\n" |
|||
"PO-Revision-Date: 2015-12-17 07:05+0000\n" |
|||
"Last-Translator: <>\n" |
|||
"Language-Team: \n" |
|||
"MIME-Version: 1.0\n" |
|||
"Content-Type: text/plain; charset=UTF-8\n" |
|||
"Content-Transfer-Encoding: \n" |
|||
"Plural-Forms: \n" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "(1, u\"3 mails not sent\")" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "0: OK" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "1: WARNING" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "2: CRITICAL" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "3: UNKNOWN" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "Any other RC value will be treated as CRITICAL." |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,nsca_args:0 |
|||
msgid "Arguments" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: model:ir.actions.act_window,name:nsca_client.action_nsca_check_tree |
|||
#: model:ir.ui.menu,name:nsca_client.menu_action_nsca_check_tree |
|||
#: view:nsca.server:nsca_client.view_nsca_server_form |
|||
#: field:nsca.server,check_ids:0 |
|||
msgid "Checks" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: code:addons/nsca_client/models/nsca_check.py:145 |
|||
#, python-format |
|||
msgid "Command '%s' not found. Please install the NSCA client.\n" |
|||
"On Debian/Ubuntu: apt-get install nsca-client" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.server,config_file_path:0 |
|||
msgid "Configuration file" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,create_uid:0 |
|||
#: field:nsca.server,create_uid:0 |
|||
msgid "Created by" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,create_date:0 |
|||
#: field:nsca.server,create_date:0 |
|||
msgid "Created on" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,cron_id:0 |
|||
msgid "Cron" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "E.g." |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "Frequency" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.server,name:0 |
|||
msgid "Hostname" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,id:0 |
|||
#: field:nsca.server,id:0 |
|||
msgid "ID" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,write_uid:0 |
|||
#: field:nsca.server,write_uid:0 |
|||
msgid "Last Updated by" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,write_date:0 |
|||
#: field:nsca.server,write_date:0 |
|||
msgid "Last Updated on" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,nsca_function:0 |
|||
msgid "Method" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,nsca_model:0 |
|||
msgid "Model" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: code:addons/nsca_client/models/nsca_check.py:60 |
|||
#: model:ir.model,name:nsca_client.model_nsca_check |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
#, python-format |
|||
msgid "NSCA Check" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_tree |
|||
msgid "NSCA Checks" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: model:ir.ui.menu,name:nsca_client.menu_nsca_client |
|||
msgid "NSCA Client" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: model:ir.model,name:nsca_client.model_nsca_server |
|||
#: view:nsca.server:nsca_client.view_nsca_server_form |
|||
msgid "NSCA Server" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.server:nsca_client.view_nsca_server_tree |
|||
msgid "NSCA Servers" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.server,port:0 |
|||
msgid "Port" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,server_id:0 |
|||
msgid "Server" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: model:ir.actions.act_window,name:nsca_client.action_nsca_server_tree |
|||
#: model:ir.ui.menu,name:nsca_client.menu_action_nsca_server_tree |
|||
msgid "Servers" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: field:nsca.check,service:0 |
|||
msgid "Service" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "Settings" |
|||
msgstr "" |
|||
|
|||
#. module: nsca_client |
|||
#: view:nsca.check:nsca_client.view_nsca_check_form |
|||
msgid "The method must return a tuple (RC, MESSAGE) where RC is an integer:" |
|||
msgstr "" |
|||
|
@ -0,0 +1 @@ |
|||
from . import nsca_check, nsca_server |
@ -0,0 +1,108 @@ |
|||
# (Copyright) 2015 ABF OSIELL <http://osiell.com> |
|||
# (Copyright) 2018 Creu Blanca |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
import logging |
|||
from odoo import _, api, fields, models |
|||
from odoo.tools.safe_eval import safe_eval |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class NscaCheck(models.Model): |
|||
_name = "nsca.check" |
|||
_description = u"NSCA Check" |
|||
_inherits = {'ir.cron': 'cron_id'} |
|||
|
|||
cron_id = fields.Many2one( |
|||
'ir.cron', string=u"Cron", |
|||
required=True, ondelete='cascade', readonly=True) |
|||
server_id = fields.Many2one( |
|||
'nsca.server', string=u"Server", required=True) |
|||
service = fields.Char(u"Service", required=True) |
|||
nsca_model = fields.Char(u"Model") |
|||
nsca_function = fields.Char(u"Method") |
|||
nsca_args = fields.Char(u"Arguments") |
|||
allow_void_result = fields.Boolean( |
|||
u"Allow void result", default=False, |
|||
help=u"By default, a CRITICAL message is sent if the method does not " |
|||
u"return.\nIf checked, no message will be sent in such a case.") |
|||
|
|||
@api.model |
|||
def default_get(self, fields_list): |
|||
"""Set some default values on the fly, without overriding fields (which |
|||
has the side effect to re-create the fields on the current model). |
|||
""" |
|||
res = super(NscaCheck, self).default_get(fields_list) |
|||
res['name'] = 'TEMP' # Required on 'ir.cron', replaced later |
|||
res['interval_number'] = 10 |
|||
res['interval_type'] = 'minutes' |
|||
return res |
|||
|
|||
@api.multi |
|||
def _force_values(self): |
|||
"""Force some values: |
|||
- Compute the name of the NSCA check to be readable |
|||
among the others 'ir.cron' records. |
|||
""" |
|||
model = self.env['ir.model'].search([('model', '=', self._name)]) |
|||
for check in self: |
|||
vals = { |
|||
'name': u"%s - %s" % (_(u"NSCA Check"), check.service), |
|||
'model_id': model.id, |
|||
'state': 'code', |
|||
'code': 'model._cron_check(%s,)' % check.id, |
|||
'doall': False, |
|||
'numbercall': -1 |
|||
} |
|||
super(NscaCheck, check).write(vals) |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
if not vals.get('model_id', False): |
|||
vals['model_id'] = self.env['ir.model'].search([ |
|||
('model', '=', self._name)]).id |
|||
if not vals.get('state', False): |
|||
vals['state'] = 'code' |
|||
check = super(NscaCheck, self).create(vals) |
|||
check._force_values() |
|||
return check |
|||
|
|||
@api.multi |
|||
def write(self, vals): |
|||
res = super(NscaCheck, self).write(vals) |
|||
if 'service' in vals: |
|||
self._force_values() |
|||
return res |
|||
|
|||
@api.model |
|||
def _cron_check(self, check_id): |
|||
self.env['nsca.server']._check_send_nsca_command() |
|||
check = self.browse(check_id) |
|||
rc, message, performance = 3, "Unknown", {} |
|||
try: |
|||
NscaModel = self.env[check.nsca_model] |
|||
results = {'model': NscaModel} |
|||
safe_eval( |
|||
'result = model.%s(%s)' % ( |
|||
check.nsca_function, check.nsca_args or ''), |
|||
results, mode="exec", nocopy=True) |
|||
result = results['result'] |
|||
if not result: |
|||
if check.allow_void_result: |
|||
return False |
|||
raise ValueError( |
|||
"'%s' method does not return" % check.nsca_function) |
|||
if len(result) == 2: |
|||
rc, message = result |
|||
else: |
|||
rc, message, performance = result |
|||
except Exception as exc: |
|||
rc, message = 2, "%s" % exc |
|||
_logger.warning("%s - %s", check.service, message) |
|||
check._send_nsca(rc, message, performance) |
|||
return True |
|||
|
|||
@api.multi |
|||
def _send_nsca(self, rc, message, performance): |
|||
for check in self: |
|||
check.server_id._send_nsca(check.service, rc, message, performance) |
@ -0,0 +1,239 @@ |
|||
# (Copyright) 2015 ABF OSIELL <http://osiell.com> |
|||
# (Copyright) 2018 Creu Blanca |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
|
|||
import psutil |
|||
import os |
|||
import shlex |
|||
import subprocess |
|||
import logging |
|||
|
|||
from odoo import api, fields, models, _ |
|||
from odoo.tools import config |
|||
from odoo.exceptions import UserError |
|||
|
|||
|
|||
def is_exe(fpath): |
|||
return os.path.isfile(fpath) and os.access(fpath, os.X_OK) |
|||
|
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
SEND_NSCA_BIN = '/usr/sbin/send_nsca' |
|||
|
|||
|
|||
class NscaServer(models.Model): |
|||
_name = "nsca.server" |
|||
_description = u"NSCA Server" |
|||
|
|||
name = fields.Char(u"Hostname", required=True) |
|||
port = fields.Integer(u"Port", default=5667, required=True) |
|||
password = fields.Char(u"Password") |
|||
encryption_method = fields.Selection( |
|||
selection='_selection_encryption_method', |
|||
string=u"Encryption method", default='1', required=True) |
|||
config_dir_path = fields.Char( |
|||
u"Configuration directory", |
|||
compute='_compute_config_dir_path') |
|||
config_file_path = fields.Char( |
|||
u"Configuration file", |
|||
compute='_compute_config_file_path') |
|||
node_hostname = fields.Char( |
|||
u"Hostname of this node", required=True, |
|||
help=u"This is the hostname of the current Odoo node declared in the " |
|||
u"monitoring server.") |
|||
check_ids = fields.One2many( |
|||
'nsca.check', 'server_id', string=u"Checks") |
|||
check_count = fields.Integer( |
|||
compute='_compute_check_count' |
|||
) |
|||
|
|||
@api.depends('check_ids') |
|||
def _compute_check_count(self): |
|||
for r in self: |
|||
r.check_count = len(r.check_ids) |
|||
|
|||
def _selection_encryption_method(self): |
|||
return [ |
|||
('0', u"0 - None (Do NOT use this option)"), |
|||
('1', u"1 - Simple XOR"), |
|||
('2', u"2 - DES"), |
|||
('3', u"3 - 3DES (Triple DES)"), |
|||
('4', u"4 - CAST-128"), |
|||
('5', u"5 - CAST-256"), |
|||
('6', u"6 - xTEA"), |
|||
('7', u"7 - 3WAY"), |
|||
('8', u"8 - BLOWFISH"), |
|||
('9', u"9 - TWOFISH"), |
|||
('10', u"10 - LOKI97"), |
|||
('11', u"11 - RC2"), |
|||
('12', u"12 - ARCFOUR"), |
|||
('14', u"14 - RIJNDAEL-128"), |
|||
('15', u"15 - RIJNDAEL-192"), |
|||
('16', u"16 - RIJNDAEL-256"), |
|||
('19', u"19 - WAKE"), |
|||
('20', u"20 - SERPENT"), |
|||
('22', u"22 - ENIGMA (Unix crypt)"), |
|||
('23', u"23 - GOST"), |
|||
('24', u"24 - SAFER64"), |
|||
('25', u"25 - SAFER128"), |
|||
('26', u"26 - SAFER+"), |
|||
] |
|||
|
|||
@api.multi |
|||
def _compute_config_dir_path(self): |
|||
for server in self: |
|||
data_dir_path = config.get('data_dir') |
|||
dir_path = os.path.join( |
|||
data_dir_path, 'nsca_client', self.env.cr.dbname) |
|||
server.config_dir_path = dir_path |
|||
|
|||
@api.multi |
|||
def _compute_config_file_path(self): |
|||
for server in self: |
|||
file_name = 'send_nsca_%s.cfg' % server.id |
|||
full_path = os.path.join(server.config_dir_path, file_name) |
|||
server.config_file_path = full_path |
|||
|
|||
@api.multi |
|||
def write_config_file(self): |
|||
for server in self: |
|||
try: |
|||
os.makedirs(server.config_dir_path) |
|||
except OSError as exception: |
|||
if exception.errno != os.errno.EEXIST: |
|||
raise |
|||
with open(server.config_file_path, 'w') as config_file: |
|||
if server.password: |
|||
config_file.write('password=%s\n' % server.password) |
|||
config_file.write( |
|||
'encryption_method=%s\n' % server.encryption_method) |
|||
return True |
|||
|
|||
@api.multi |
|||
def write(self, vals): |
|||
res = super(NscaServer, self).write(vals) |
|||
self.write_config_file() |
|||
return res |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
res = super(NscaServer, self).create(vals) |
|||
res.write_config_file() |
|||
return res |
|||
|
|||
@api.model |
|||
def current_status(self): |
|||
ram = 0 |
|||
cpu = 0 |
|||
if psutil: |
|||
process = psutil.Process(os.getpid()) |
|||
# psutil changed its api through versions |
|||
processes = [process] |
|||
if config.get( |
|||
'workers') and process.parent: # pragma: no cover |
|||
if hasattr(process.parent, '__call__'): |
|||
process = process.parent() |
|||
else: |
|||
process = process.parent |
|||
if hasattr(process, 'children'): |
|||
processes += process.children(True) |
|||
elif hasattr(process, 'get_children'): |
|||
processes += process.get_children(True) |
|||
for process in processes: |
|||
if hasattr(process, 'memory_percent'): |
|||
ram += process.memory_percent() |
|||
if hasattr(process, 'cpu_percent'): |
|||
cpu += process.cpu_percent(interval=1) |
|||
user_count = 0 |
|||
if 'bus.presence' in self.env.registry: |
|||
user_count = self.env['bus.presence'].search_count([ |
|||
('status', '=', 'online'), |
|||
]) |
|||
performance = { |
|||
'cpu': { |
|||
'value': cpu, |
|||
}, |
|||
'ram': { |
|||
'value': ram, |
|||
}, |
|||
'user_count': { |
|||
'value': user_count, |
|||
}, |
|||
} |
|||
return 0, u"OK", performance |
|||
|
|||
@api.multi |
|||
def _prepare_command(self): |
|||
"""Prepare the shell command used to send the check result |
|||
to the NSCA daemon. |
|||
""" |
|||
cmd = u"/usr/sbin/send_nsca -H %s -p %s -c %s" % ( |
|||
self.name, |
|||
self.port, |
|||
self.config_file_path) |
|||
return shlex.split(cmd) |
|||
|
|||
@api.model |
|||
def _run_command(self, cmd, check_result): |
|||
"""Send the check result through the '/usr/sbin/send_nsca' command.""" |
|||
try: |
|||
proc = subprocess.Popen( |
|||
cmd, |
|||
stdout=subprocess.PIPE, |
|||
stdin=subprocess.PIPE, |
|||
stderr=subprocess.STDOUT) |
|||
stdout = proc.communicate( |
|||
input=check_result)[0] |
|||
_logger.debug("%s: %s", check_result, stdout.strip()) |
|||
except Exception as exc: |
|||
_logger.error(exc) |
|||
|
|||
def _check_send_nsca_command(self): |
|||
"""Check if the NSCA client is installed.""" |
|||
if not is_exe(SEND_NSCA_BIN): |
|||
raise UserError( |
|||
_(u"Command '%s' not found. Please install the NSCA client.\n" |
|||
u"On Debian/Ubuntu: apt-get install nsca-client") % ( |
|||
SEND_NSCA_BIN)) |
|||
|
|||
def _format_check_result(self, service, rc, message): |
|||
"""Format the check result with tabulations as delimiter.""" |
|||
message = message.replace('\t', ' ') |
|||
hostname = self.node_hostname |
|||
check_result = u"%s\t%s\t%s\t%s" % ( |
|||
hostname, service, rc, message) |
|||
return check_result.encode('utf-8') |
|||
|
|||
def _send_nsca(self, service, rc, message, performance): |
|||
"""Send the result of the check to the NSCA daemon.""" |
|||
msg = message |
|||
if len(performance) > 0: |
|||
msg += '| ' + ''.join( |
|||
["%s=%s%s;%s;%s;%s;%s" % ( |
|||
key, |
|||
performance[key]['value'], |
|||
performance[key].get('uom', ''), |
|||
performance[key].get('warn', ''), |
|||
performance[key].get('crit', ''), |
|||
performance[key].get('min', ''), |
|||
performance[key].get('max', ''), |
|||
) for key in sorted(performance)]) |
|||
check_result = self._format_check_result( |
|||
service, rc, msg) |
|||
cmd = self._prepare_command() |
|||
self._run_command(cmd, check_result) |
|||
|
|||
@api.multi |
|||
def show_checks(self): |
|||
self.ensure_one() |
|||
action = self.env.ref('nsca_client.action_nsca_check_tree') |
|||
result = action.read()[0] |
|||
context = {'default_server_id': self.id} |
|||
result['context'] = context |
|||
result['domain'] = [('server_id', '=', self.id)] |
|||
if len(self.check_ids) == 1: |
|||
res = self.env.ref('nsca_client.view_nsca_check_form', False) |
|||
result['views'] = [(res and res.id or False, 'form')] |
|||
result['res_id'] = self.check_ids.id |
|||
return result |
@ -0,0 +1,3 @@ |
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
|||
access_nsca_check,access_nsca_check,model_nsca_check,base.group_erp_manager,1,1,1,1 |
|||
access_nsca_server,access_nsca_server,model_nsca_server,base.group_erp_manager,1,1,1,1 |
After Width: 707 | Height: 448 | Size: 43 KiB |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
After Width: 653 | Height: 521 | Size: 32 KiB |
@ -0,0 +1 @@ |
|||
from . import test_nsca |
@ -0,0 +1,104 @@ |
|||
# Copyright 2018 Creu Blanca |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
import mock |
|||
|
|||
from odoo.tests.common import TransactionCase |
|||
|
|||
|
|||
class Popen: |
|||
def __init__(self, cmd, stdout, stdin, stderr): |
|||
self.cmd = cmd |
|||
self.stdout = stdout |
|||
self.stdin = stdin |
|||
self.stderr = stderr |
|||
|
|||
def communicate(input): |
|||
return ['test'] |
|||
|
|||
|
|||
class TestNsca(TransactionCase): |
|||
|
|||
def test_nsca(self): |
|||
server = self.env['nsca.server'].create({ |
|||
'name': 'localhost', |
|||
'password': 'pass', |
|||
'encryption_method': '3', |
|||
'node_hostname': 'odoodev', |
|||
}) |
|||
self.assertTrue(server.config_file_path) |
|||
with mock.patch('subprocess.Popen') as post: |
|||
post.return_value = Popen |
|||
check = self.env['nsca.check'].create({ |
|||
'server_id': server.id, |
|||
'service': 'test', |
|||
'nsca_model': 'nsca.server', |
|||
'nsca_function': 'current_status' |
|||
}) |
|||
self.assertTrue(check.model_id) |
|||
self.env['nsca.check']._cron_check(check.id,) |
|||
|
|||
def test_write(self): |
|||
server = self.env['nsca.server'].create({ |
|||
'name': 'localhost', |
|||
'password': 'pass', |
|||
'encryption_method': '3', |
|||
'node_hostname': 'odoodev', |
|||
}) |
|||
self.assertTrue(server.config_file_path) |
|||
check = self.env['nsca.check'].create({ |
|||
'server_id': server.id, |
|||
'service': 'test', |
|||
'nsca_model': 'nsca.server', |
|||
'nsca_function': 'current_status' |
|||
}) |
|||
check.cron_id.state = 'object_create' |
|||
check.write({'interval_number': 1}) |
|||
self.assertEqual(check.cron_id.state, 'object_create') |
|||
check.write({'service': 'change'}) |
|||
self.assertNotEqual(check.cron_id.state, 'object_create') |
|||
|
|||
def test_void_failure(self): |
|||
server = self.env['nsca.server'].create({ |
|||
'name': 'localhost', |
|||
'password': 'pass', |
|||
'encryption_method': '3', |
|||
'node_hostname': 'odoodev', |
|||
}) |
|||
check = self.env['nsca.check'].create({ |
|||
'server_id': server.id, |
|||
'service': 'test', |
|||
'nsca_model': 'nsca.server', |
|||
'allow_void_result': False, |
|||
'nsca_function': '_check_send_nsca_command' |
|||
}) |
|||
with mock.patch('subprocess.Popen') as post: |
|||
post.return_value = Popen |
|||
self.env['nsca.check']._cron_check(check.id,) |
|||
post.assert_called_once() |
|||
|
|||
def test_void_ok(self): |
|||
server = self.env['nsca.server'].create({ |
|||
'name': 'localhost', |
|||
'password': 'pass', |
|||
'encryption_method': '3', |
|||
'node_hostname': 'odoodev', |
|||
}) |
|||
self.assertEqual(server.check_count, 0) |
|||
check = self.env['nsca.check'].create({ |
|||
'server_id': server.id, |
|||
'service': 'test', |
|||
'nsca_model': 'nsca.server', |
|||
'allow_void_result': True, |
|||
'nsca_function': '_check_send_nsca_command' |
|||
}) |
|||
self.assertEqual(server.check_count, 1) |
|||
action = server.show_checks() |
|||
self.assertEqual(check, self.env['nsca.check'].browse( |
|||
action['res_id'])) |
|||
self.assertEqual(check, self.env['nsca.check'].search( |
|||
action['domain'])) |
|||
with mock.patch('subprocess.Popen') as post: |
|||
post.return_value = Popen |
|||
self.env['nsca.check']._cron_check(check.id,) |
|||
post.assert_not_called() |
@ -0,0 +1,90 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- © 2015 ABF OSIELL <http://osiell.com> |
|||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> |
|||
|
|||
<odoo> |
|||
<data> |
|||
|
|||
<record id="view_nsca_check_form" model="ir.ui.view"> |
|||
<field name="name">nsca.check.form</field> |
|||
<field name="model">nsca.check</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="NSCA Check"> |
|||
<sheet> |
|||
<group> |
|||
<group string="NSCA Check"> |
|||
<field name="server_id"/> |
|||
<field name="service"/> |
|||
<label for="interval_number" |
|||
string="Frequency"/> |
|||
<div> |
|||
<field name="interval_number" |
|||
class="oe_inline"/> |
|||
<field name="interval_type" |
|||
class="oe_inline"/> |
|||
</div> |
|||
<field name="nextcall"/> |
|||
<field name="active"/> |
|||
</group> |
|||
<group string="Settings"> |
|||
<field name="nsca_model"/> |
|||
<field name="nsca_function"/> |
|||
<field name="nsca_args" widget="ace" options="{'mode': 'python'}" /> |
|||
<field name="allow_void_result"/> |
|||
<div colspan="2"> |
|||
<p>The method must return a tuple (RC, |
|||
MESSAGE) where RC is an integer: |
|||
</p> |
|||
<newline/> |
|||
<ul> |
|||
<li>0: OK</li> |
|||
<li>1: WARNING</li> |
|||
<li>2: CRITICAL</li> |
|||
<li>3: UNKNOWN</li> |
|||
</ul> |
|||
<newline/> |
|||
<p>Any other RC value will be treated as |
|||
CRITICAL. |
|||
</p> |
|||
<p>E.g. |
|||
<em>(1, u"3 mails not sent")</em> |
|||
</p> |
|||
</div> |
|||
</group> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="view_nsca_check_tree" model="ir.ui.view"> |
|||
<field name="name">nsca.check.tree</field> |
|||
<field name="model">nsca.check</field> |
|||
<field name="arch" type="xml"> |
|||
<tree string="NSCA Checks" colors="gray: active==False"> |
|||
<field name="service"/> |
|||
<field name="server_id"/> |
|||
<field name="nextcall"/> |
|||
<field name="active"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.actions.act_window" id="action_nsca_check_tree"> |
|||
<field name="name">Checks</field> |
|||
<field name="type">ir.actions.act_window</field> |
|||
<field name="res_model">nsca.check</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_id" ref="view_nsca_check_tree"/> |
|||
<field name="context" eval="{'default_active': True}"/> |
|||
<field name="domain">['|', ('active', '=', True), ('active', '=', |
|||
False)] |
|||
</field> |
|||
</record> |
|||
|
|||
<menuitem id="menu_action_nsca_check_tree" |
|||
parent="menu_nsca_client" |
|||
action="action_nsca_check_tree"/> |
|||
|
|||
</data> |
|||
</odoo> |
@ -0,0 +1,12 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- © 2015 ABF OSIELL <http://osiell.com> |
|||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> |
|||
<odoo> |
|||
<data> |
|||
|
|||
<menuitem id="menu_nsca_client" |
|||
parent="base.menu_custom" |
|||
name="NSCA Client"/> |
|||
|
|||
</data> |
|||
</odoo> |
@ -0,0 +1,63 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- © 2015 ABF OSIELL <http://osiell.com> |
|||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> |
|||
|
|||
<odoo> |
|||
<data> |
|||
|
|||
<record id="view_nsca_server_form" model="ir.ui.view"> |
|||
<field name="name">nsca.server.form</field> |
|||
<field name="model">nsca.server</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="NSCA Server"> |
|||
<sheet> |
|||
<div class="oe_button_box" name="button_box"> |
|||
<button name="show_checks" type="object" |
|||
class="oe_stat_button" |
|||
icon="fa-check-square"> |
|||
<field name="check_count" widget="statinfo" |
|||
string="Checks"/> |
|||
<field name="check_ids" invisible="1"/> |
|||
</button> |
|||
</div> |
|||
<group string="NSCA Server"> |
|||
<field name="name" style="width: 30%;"/> |
|||
<field name="port"/> |
|||
<field name="password" password="1"/> |
|||
<field name="encryption_method"/> |
|||
<field name="config_file_path"/> |
|||
</group> |
|||
<group string="Node identity"> |
|||
<field name="node_hostname"/> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="view_nsca_server_tree" model="ir.ui.view"> |
|||
<field name="name">nsca.server.tree</field> |
|||
<field name="model">nsca.server</field> |
|||
<field name="arch" type="xml"> |
|||
<tree string="NSCA Servers"> |
|||
<field name="name"/> |
|||
<field name="port"/> |
|||
<field name="config_file_path"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.actions.act_window" id="action_nsca_server_tree"> |
|||
<field name="name">Servers</field> |
|||
<field name="type">ir.actions.act_window</field> |
|||
<field name="res_model">nsca.server</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_id" ref="view_nsca_server_tree"/> |
|||
</record> |
|||
|
|||
<menuitem id="menu_action_nsca_server_tree" |
|||
parent="menu_nsca_client" |
|||
action="action_nsca_server_tree"/> |
|||
|
|||
</data> |
|||
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue