diff --git a/connector_dns/README.rst b/connector_dns/README.rst new file mode 100755 index 0000000..6b39e96 --- /dev/null +++ b/connector_dns/README.rst @@ -0,0 +1,85 @@ +.. 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 + +============= +Connector DNS +============= + +This module aims to allows to manage your DNS domain through Odoo. + +Installation +============ + +To install this module, you need to: + + * have basic modules installed (connector) + +Configuration +============= + +To configure this module, you need to: + +#. Go to ... + +.. figure:: path/to/local/image.png + :alt: alternative description + :width: 600 px + +Usage +===== + +To use this module, you need to: + +#. Go to ... + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/{repo_id}/{branch} + +.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt +.. branch is "8.0" for example + +Known issues / Roadmap +====================== + +* TBD + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Eric Caudal +* Noah Wang +* Liu Lixia +* Augustin Cisterne-Kaas + +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. diff --git a/connector_dns/__init__.py b/connector_dns/__init__.py new file mode 100755 index 0000000..b9e4e7d --- /dev/null +++ b/connector_dns/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import backend +from . import connector +from . import dns +from . import tests +from . import unit + diff --git a/connector_dns/__openerp__.py b/connector_dns/__openerp__.py new file mode 100755 index 0000000..fd2817f --- /dev/null +++ b/connector_dns/__openerp__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + 'name': 'DNS connector', + 'version': '8.0.1.0.0', + 'category': 'Connector', + 'depends': ['connector'], + 'author': 'Elico Corp,Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': 'https://www.elico-corp.com', + 'images': [], + 'data': [ + 'dns_view.xml', + 'dns_menu.xml' + ], + 'installable': True, + 'application': False +} diff --git a/connector_dns/backend.py b/connector_dns/backend.py new file mode 100755 index 0000000..76c3868 --- /dev/null +++ b/connector_dns/backend.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import openerp.addons.connector.backend as backend + +dns = backend.Backend('dns') diff --git a/connector_dns/connector.py b/connector_dns/connector.py new file mode 100755 index 0000000..f17e89f --- /dev/null +++ b/connector_dns/connector.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import models, fields +from openerp.addons.connector.connector import (Environment) +from openerp.addons.connector.checkpoint import checkpoint + + +def get_environment(session, model_name, backend_id): + """ Create an environment to work with. """ + backend_record = session.browse('dns.backend', backend_id) + env = Environment(backend_record, session, model_name) + return env + + +class DNSBinding(models.Model): + """ Abstract Model for the Bindigs. + All the models used as bindings between dnspod and OpenERP + (``dnspod.res.partner``, ``dnspod.product.product``, ...) should + ``_inherit`` it. + """ + _name = 'dns.binding' + _inherit = 'external.binding' + _description = 'dns Binding (abstract)' + + backend_id = fields.Many2one( + 'dns.backend', + String='DNS Backend', + required=True, + ondelete='restrict') + # fields.char because 0 is a valid dnspod ID + dns_id = fields.Char('ID on other software') + # state of the record synchronization with dnspod + state = fields.Selection( + [('draft', 'Draft'), ('done', 'Done'), + ('exception', 'Exception')], 'State', + default="draft", + help='Done when succeed otherwise Exception') + + +def add_checkpoint(session, model_name, record_id, backend_id): + """ Add a row in the model ``connector.checkpoint`` for a record, + meaning it has to be reviewed by a user. + :param session: current session + :type session: :class:`openerp.addons.connector.session.ConnectorSession` + :param model_name: name of the model of the record to be reviewed + :type model_name: str + :param record_id: ID of the record to be reviewed + :type record_id: int + :param backend_id: ID of the dnspod Backend + :type backend_id: int + """ + return checkpoint.add_checkpoint(session, model_name, record_id, + 'dns.backend', backend_id) diff --git a/connector_dns/dns.py b/connector_dns/dns.py new file mode 100755 index 0000000..e535dd6 --- /dev/null +++ b/connector_dns/dns.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import models, fields, api + + +class DNSBackend(models.Model): + _name = 'dns.backend' + _inherit = 'connector.backend' + _backend_type = 'dns' + + def _select_version(self): + return [] + + login = fields.Char( + string='Login', + help="Provider's login.", + required=True + ) + password = fields.Char( + string='Password', + help="Provider's password.", + required=True + ) + state = fields.Selection( + [('draft', 'Draft'), ('done', 'Done'), + ('exception', 'Exception')], + string='State', + default="draft", + help='"Confirmed" when the domain has been succesfully created.' + ) + version = fields.Selection( + selection='_select_version', + string='Service Provider', + help='DNS service provider', + required=True + ) + + @api.multi + def name_get(self): + res = [] + for backend in self: + res.append((backend.id, '%s (%s)' % (backend.name, backend.login))) + return res + + +class DNSDomain(models.Model): + _name = 'dns.domain' + _inherit = 'dns.binding' + + name = fields.Char( + string='Name', + required=True, + help='Domain name without "www",such as"dnspod.cn"' + ) + record_ids = fields.One2many( + comodel_name='dns.record', + inverse_name='domain_id', + string='Subdomains' + ) + + +class DNSRecord(models.Model): + _name = 'dns.record' + _inherit = 'dns.binding' + + def _line_select_version(self): + return [] + + def _type_select_version(self): + return [] + + name = fields.Char( + string='Sub domain', + help="host record,such as 'www'", + required=True) + domain_id = fields.Many2one( + comodel_name='dns.domain', + string="Domain", + domain="[('state','=','done')]", + ondelete='cascade', + help="Domain which has already confirmed" + ) + type = fields.Selection( + selection='_type_select_version', + string='Record Type' + ) + line = fields.Selection( + selection='_line_select_version', + string='Record Line' + ) + value = fields.Text( + string='Value', + help="such as IP:200.200.200.200", + required=True + ) + mx_priority = fields.Integer( + string='MX priority', + help="scope:1-20", + default=1 + ) + ttl = fields.Integer( + string='TTL', + default=600, + help="scope:1-604800", + required=True + ) + backend_id = fields.Many2one( + comodel_name='dns.backend', + related='domain_id.backend_id', + store=True + ) diff --git a/connector_dns/dns_menu.xml b/connector_dns/dns_menu.xml new file mode 100755 index 0000000..1325c07 --- /dev/null +++ b/connector_dns/dns_menu.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/connector_dns/dns_view.xml b/connector_dns/dns_view.xml new file mode 100755 index 0000000..e26d931 --- /dev/null +++ b/connector_dns/dns_view.xml @@ -0,0 +1,162 @@ + + + + + + DNS Backend + dns.backend + ir.actions.act_window + form + tree,form + + + + DNS Backend form + dns.backend + + + + + + + + + DNS Backend form + dns.backend + +
+
+
+ +
+
+ + + + + +
+
+
+
+ + + + DNS Domain Action + dns.domain + form + tree,form + + + + DNS domain tree + dns.domain + + + + + + + + + + + DNS domain form + dns.domain + +
+
+
+ +
+
+ + + +
+
+
+
+ + + + + DNS Record Action + dns.record + form + tree,form + + + + DNS Record tree + dns.record + + + + + + + + + + + + + + + + DNS Record form + dns.record + +
+
+ +
+ +
+
+ + + + + + + + + +
+
+
+
+
+
diff --git a/connector_dns/tests/__init__.py b/connector_dns/tests/__init__.py new file mode 100644 index 0000000..582a76b --- /dev/null +++ b/connector_dns/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_backend diff --git a/connector_dns/tests/test_backend.py b/connector_dns/tests/test_backend.py new file mode 100644 index 0000000..fa01f16 --- /dev/null +++ b/connector_dns/tests/test_backend.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import openerp.tests.common as common +from openerp.addons.connector.backend import Backend + + +class TestBackend(common.TransactionCase): + """ + test generic Backend + """ + + def setUp(self): + super(TestBackend, self).setUp() + self.service = "dns" + self.version = "1.7" + + def tearDown(self): + super(TestBackend, self).tearDown() + + def test_dnspod(self): + dnspod = Backend(self.service) + self.assertEqual(dnspod.service, self.service) + + def test_child_dnspod(self): + dnspod = Backend(self.service) + child_dnspod = Backend(parent=dnspod, version=self.service) + self.assertEqual(child_dnspod.service, dnspod.service) diff --git a/connector_dns/unit/__init__.py b/connector_dns/unit/__init__.py new file mode 100755 index 0000000..2b0da66 --- /dev/null +++ b/connector_dns/unit/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import backend_adapter +from . import binder +from . import export_synchronizer diff --git a/connector_dns/unit/backend_adapter.py b/connector_dns/unit/backend_adapter.py new file mode 100755 index 0000000..e1d55ff --- /dev/null +++ b/connector_dns/unit/backend_adapter.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +from openerp.addons.connector.unit.backend_adapter import CRUDAdapter + +_logger = logging.getLogger(__name__) + + +recorder = {} + + +def call_to_key(method, arguments): + """ Used to 'freeze' the method and arguments of a call to DNS + so they can be hashable; they will be stored in a dict. + + Used in both the recorder and the tests. + """ + def freeze(arg): + if isinstance(arg, dict): + items = dict((key, freeze(value)) for key, value + in arg.iteritems()) + return frozenset(items.iteritems()) + elif isinstance(arg, list): + return tuple([freeze(item) for item in arg]) + else: + return arg + + new_args = [] + for arg in arguments: + new_args.append(freeze(arg)) + return (method, tuple(new_args)) + + +def record(method, arguments, result): + """ Utility function which can be used to record test data + during synchronisations. Call it from DNSAdapter._call + + Then ``output_recorder`` can be used to write the data recorded + to a file. + """ + recorder[call_to_key(method, arguments)] = result + + +def output_recorder(filename): + import pprint + with open(filename, 'w') as f: + pprint.pprint(recorder, f) + _logger.debug('recorder written to file %s', filename) + + +class DNSLocation(object): + + def __init__(self, login, password): + self.login = login + self.password = password + + +class DNSAdapter(CRUDAdapter): + """ External Records Adapter for DNS """ + + def __init__(self, environment): + """ + + :param environment: current environment (backend, session, ...) + :type environment: :py:class:`connector.connector.Environment` + """ + super(DNSAdapter, self).__init__(environment) + self.DNS = DNSLocation( + self.backend_record.login, self.backend_record.password) + + def search(self, filters=None): + """ Search records according to some criterias + and returns a list of ids """ + raise NotImplementedError + + def read(self, id, attributes=None): + """ Returns the information of a record """ + raise NotImplementedError + + def search_read(self, filters=None): + """ Search records according to some criterias + and returns their information""" + raise NotImplementedError + + def create(self, data): + raise NotImplementedError + + def write(self, data): + """ Update records on the external system """ + raise NotImplementedError + + def delete(self, data): + """ Delete a record on the external system """ + raise NotImplementedError + + def _call(self, action, arguments): + raise NotImplementedError diff --git a/connector_dns/unit/binder.py b/connector_dns/unit/binder.py new file mode 100755 index 0000000..47583ce --- /dev/null +++ b/connector_dns/unit/binder.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import datetime +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.addons.connector.connector import Binder +from ..backend import dns + + +class DNSPodBinder(Binder): + """ Generic Binder for DNSPod """ + + +@dns +class DNSPodModelBinder(DNSPodBinder): + """ + Bindings are done directly on the binding model. + + Binding models are models called ``dns.{normal_model}``, + like ``dns.record`` or ``dns.domain``. + They are ``_inherits`` of the normal models and contains + the DNS ID, the ID of the DNS Backend and the additional + fields belonging to the DNS instance. + """ + _model_name = [ + 'dns.record', + 'dns.domain' + ] + + def to_openerp(self, external_id, unwrap=False): + """ Give the OpenERP ID for an external ID + + :param external_id: external ID for which we want the OpenERP ID + :param unwrap: if True, returns the openerp_id of the dns_xx record, + else return the id (binding id) of that record + :return: a record ID, depending on the value of unwrap, + or None if the external_id is not mapped + :rtype: int + """ + binding_ids = self.session.search( + self.model._name, + [('dns_id', '=', str(external_id)), + ('backend_id', '=', self.backend_record.id)]) + if not binding_ids: + return None + assert len(binding_ids) == 1, "Several records found: %s" % binding_ids + binding_id = binding_ids[0] + if unwrap: + return self.session.read(self.model._name, + binding_id, + ['openerp_id'])['openerp_id'][0] + else: + return binding_id + + def to_backend(self, binding_id): + """ Give the external ID for an OpenERP ID + + :param binding_id: OpenERP ID for which we want the external id + :return: backend identifier of the record + """ + dns_record = self.session.read( + self.model._name, binding_id, ['dns_id']) + assert dns_record + return dns_record['dns_id'] + + def bind(self, external_id, binding_id): + """ Create the link between an external ID and an OpenERP ID and + update the last synchronization date. + + :param external_id: External ID to bind + :param binding_id: OpenERP ID to bind + :type binding_id: int + """ + # avoid to trigger the export when we modify the `dns_id` + model = self.model.with_context(connector_no_export=True) + binding = model.browse(binding_id) + now_fmt = datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT) + if external_id: + state = 'done' + else: + state = 'exception' + binding.write({'dns_id': str(external_id), + 'state': state, + 'sync_date': now_fmt}) diff --git a/connector_dns/unit/export_synchronizer.py b/connector_dns/unit/export_synchronizer.py new file mode 100755 index 0000000..ce1d25b --- /dev/null +++ b/connector_dns/unit/export_synchronizer.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Elico Corp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +from openerp.addons.connector.unit.synchronizer import ExportSynchronizer + + +_logger = logging.getLogger(__name__) + + +""" + +Exporters for DNS. + +In addition to its export job, an exporter has to: + +* check in DNS if the record has been updated more recently than the + last sync date and if yes, delay an import +* call the ``bind`` method of the binder to update the last sync date + +""" + + +class DNSBaseExporter(ExportSynchronizer): + + """ Base exporter for DNS """ + + def __init__(self, environment): + """ + :param environment: current environment (backend, session, ...) + :type environment: :py:class:`connector.connector.Environment` + """ + super(DNSBaseExporter, self).__init__(environment) + self.binding_id = None + self.external_id = None + + def _get_openerp_data(self): + """ Return the raw OpenERP data for ``self.binding_id`` """ + return self.session.browse(self.model._name, self.binding_id) + + def run(self, binding_id, *args, **kwargs): + """ Run the synchronization + + :param binding_id: identifier of the binding record to export + """ + self.binding_id = binding_id + self.binding_record = self._get_openerp_data() + + self.external_id = self.binder.to_backend(self.binding_id) + result = self._run(*args, **kwargs) + + self.binder.bind(self.external_id, self.binding_id) + return result + + def _run(self): + """ Flow of the synchronization, implemented in inherited classes""" + raise NotImplementedError diff --git a/oca_dependencies.txt b/oca_dependencies.txt index 865a6a1..5428a2d 100644 --- a/oca_dependencies.txt +++ b/oca_dependencies.txt @@ -1,2 +1 @@ connector -