From 8224a72cef8da7c651864dc3471a2aee307e57bb Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Wed, 25 May 2016 09:24:59 -0700 Subject: [PATCH 01/57] [ADD] letsencrypt (#425) * [ADD] letsencrypt (#347) * [ADD] letsencrypt * [ADD] write bogus restart script for tests * [IMP] exclude library call from coveralls * [IMP] try moving the library import into nocover branch * [ADD] explain how to redirect the well known uri to the odoo instance * [ADD] example for apache * [FIX] cronjob should be noupdate * [FIX] community review * [FIX] flake8 * [DEL] unused imports * [UPD] chain cert * Multi-database support and other fixes (#2) [ADD] multi-database support and other fixes * [ADD] eggs necessary for letsencrypt * [IMP] readme * [ADD] ipv6 localhosts * [ADD] restrict reload command * Revert "[ADD] eggs necessary for letsencrypt" This reverts commit 642df6ba50c319dff3c2e546034c14329c2443e0. * [ADD] eggs necessary for letsencrypt Conflicts: requirements.txt * Migrate letsencrypt to v9 * Add AGPL target link to ReadMe in letsencrypt --- letsencrypt/README.rst | 166 ++++++++++++++++++++++ letsencrypt/__init__.py | 6 + letsencrypt/__openerp__.py | 31 ++++ letsencrypt/controllers/__init__.py | 4 + letsencrypt/controllers/main.py | 19 +++ letsencrypt/data/ir_config_parameter.xml | 10 ++ letsencrypt/data/ir_cron.xml | 14 ++ letsencrypt/hooks.py | 9 ++ letsencrypt/models/__init__.py | 4 + letsencrypt/models/letsencrypt.py | 171 +++++++++++++++++++++++ letsencrypt/static/description/icon.png | Bin 0 -> 2157 bytes letsencrypt/tests/__init__.py | 4 + letsencrypt/tests/test_letsencrypt.py | 14 ++ 13 files changed, 452 insertions(+) create mode 100644 letsencrypt/README.rst create mode 100644 letsencrypt/__init__.py create mode 100644 letsencrypt/__openerp__.py create mode 100644 letsencrypt/controllers/__init__.py create mode 100644 letsencrypt/controllers/main.py create mode 100644 letsencrypt/data/ir_config_parameter.xml create mode 100644 letsencrypt/data/ir_cron.xml create mode 100644 letsencrypt/hooks.py create mode 100644 letsencrypt/models/__init__.py create mode 100644 letsencrypt/models/letsencrypt.py create mode 100644 letsencrypt/static/description/icon.png create mode 100644 letsencrypt/tests/__init__.py create mode 100644 letsencrypt/tests/test_letsencrypt.py diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst new file mode 100644 index 00000000000..dfcd02accfb --- /dev/null +++ b/letsencrypt/README.rst @@ -0,0 +1,166 @@ +.. 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 + +============================================= +Request SSL certificates from letsencrypt.org +============================================= + +This module was written to have your Odoo installation request SSL certificates +from https://letsencrypt.org automatically. + +Installation +============ + +After installation, this module generates a private key for your account at +letsencrypt.org automatically in ``$data_dir/letsencrypt/account.key``. If you +want or need to use your own account key, replace the file. + +For certificate requests to work, your site needs to be accessible via plain +HTTP, see below for configuration examples in case you force your clients to +the SSL version. + +After installation, trigger the cronjob `Update letsencrypt certificates` and +watch your log for messages. + +This addon depends on the ``openssl`` binary and the ``acme_tiny`` and ``IPy`` +python modules. + +For installing the OpenSSL binary you can use your distro package manager. +For Debian and Ubuntu, that would be: + + sudo apt-get install openssl + +For installing the ACME-Tiny python module, use the PIP package manager: + + sudo pip install acme-tiny + +For installing the IPy python module, use the PIP package manager: + + sudo pip install IPy + + +Configuration +============= + +This addons requests a certificate for the domain named in the configuration +parameter ``web.base.url`` - if this comes back as ``localhost`` or the like, +the module doesn't request anything. + +If you want your certificate to contain multiple alternative names, just add +them as configuration parameters ``letsencrypt.altname.N`` with ``N`` starting +from ``0``. The amount of domains that can be added are subject to `rate +limiting `_. + +Note that all those domains must be publicly reachable on port 80 via HTTP, and +they must have an entry for ``.well-known/acme-challenge`` pointing to your odoo +instance. + +Usage +===== + +The module sets up a cronjob that requests and renews certificates automatically. + +After the first run, you'll find a file called ``domain.crt`` in +``$datadir/letsencrypt``, configure your SSL proxy to use this file as certificate. + +.. 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/8.0 + +For further information, please visit: + +* https://www.odoo.com/forum/help-1 + +In depth configuration +====================== + +This module uses ``openssl`` to generate CSRs suitable to be submitted to +letsencrypt.org. In order to do this, it copies ``/etc/ssl/openssl.cnf`` to a +temporary and adapts it according to its needs (currently, that's just adding a +``[SAN]`` section if necessary). If you want the module to use another configuration +template, set config parameter ``letsencrypt.openssl.cnf``. + +After refreshing the certificate, the module attempts to run the content of +``letsencrypt.reload_command``, which is by default ``sudo service nginx reload``. +Change this to match your server's configuration. + +You'll also need a matching sudo configuration, like:: + + your_odoo_user ALL = NOPASSWD: /usr/sbin/service nginx reload + +Further, if you force users to https, you'll need something like for nginx:: + + if ($scheme = "http") { + set $redirect_https 1; + } + if ($request_uri ~ ^/.well-known/acme-challenge/) { + set $redirect_https 0; + } + if ($redirect_https) { + rewrite ^ https://$server_name$request_uri? permanent; + } + +and this for apache:: + + RewriteEngine On + RewriteCond %{HTTPS} !=on + RewriteCond %{REQUEST_URI} "!^/.well-known/" + RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L] + +In case you need to redirect other nginx sites to your Odoo instance, declare +an upstream for your odoo instance and do something like:: + + location /.well-known { + proxy_pass http://yourodooupstream; + } + +If you're using a multi-database installation (with or without dbfilter option) +where /web/databse/selector returns a list of more than one database, then +you need to add ``letsencrypt`` addon to wide load addons list +(by default, only ``web`` addon), setting ``--load`` option. +For example, ``--load=web,letsencrypt`` + + +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 +`here `_. + +Credits +======= + +Contributors +------------ + +* Holger Brunn +* Antonio Espinosa +* Dave Lasley + +ACME implementation +------------------- + +* https://github.com/diafygi/acme-tiny/blob/master/acme_tiny.py + +Icon +---- + +* https://helloworld.letsencrypt.org + +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/letsencrypt/__init__.py b/letsencrypt/__init__.py new file mode 100644 index 00000000000..5a561ec1084 --- /dev/null +++ b/letsencrypt/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models +from . import controllers +from .hooks import post_init_hook diff --git a/letsencrypt/__openerp__.py b/letsencrypt/__openerp__.py new file mode 100644 index 00000000000..01457b8073a --- /dev/null +++ b/letsencrypt/__openerp__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Let's encrypt", + "version": "9.0.1.0.0", + "author": "Therp BV," + "Tecnativa," + "Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Hidden/Dependency", + "summary": "Request SSL certificates from letsencrypt.org", + "depends": [ + 'base', + ], + "data": [ + "data/ir_config_parameter.xml", + "data/ir_cron.xml", + ], + "post_init_hook": 'post_init_hook', + "installable": True, + "external_dependencies": { + 'bin': [ + 'openssl', + ], + 'python': [ + 'acme_tiny', + 'IPy', + ], + }, +} diff --git a/letsencrypt/controllers/__init__.py b/letsencrypt/controllers/__init__.py new file mode 100644 index 00000000000..b0d7e8a3843 --- /dev/null +++ b/letsencrypt/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import main diff --git a/letsencrypt/controllers/main.py b/letsencrypt/controllers/main.py new file mode 100644 index 00000000000..03b8c4d15a6 --- /dev/null +++ b/letsencrypt/controllers/main.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# © 2016 Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import os +from openerp import http +from openerp.http import request +from ..models.letsencrypt import get_challenge_dir + + +class Letsencrypt(http.Controller): + @http.route('/.well-known/acme-challenge/', auth='none') + def acme_challenge(self, filename): + try: + with file(os.path.join(get_challenge_dir(), filename)) as key: + return key.read() + except IOError: + pass + return request.not_found() diff --git a/letsencrypt/data/ir_config_parameter.xml b/letsencrypt/data/ir_config_parameter.xml new file mode 100644 index 00000000000..eb3c3f54b8d --- /dev/null +++ b/letsencrypt/data/ir_config_parameter.xml @@ -0,0 +1,10 @@ + + + + + letsencrypt.reload_command + sudo /usr/sbin/service nginx reload + + + + diff --git a/letsencrypt/data/ir_cron.xml b/letsencrypt/data/ir_cron.xml new file mode 100644 index 00000000000..1b43f86092c --- /dev/null +++ b/letsencrypt/data/ir_cron.xml @@ -0,0 +1,14 @@ + + + + + Update letsencrypt certificates + weeks + 11 + -1 + letsencrypt + cron + 2016-01-01 + + + diff --git a/letsencrypt/hooks.py b/letsencrypt/hooks.py new file mode 100644 index 00000000000..3332fa23f71 --- /dev/null +++ b/letsencrypt/hooks.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp import SUPERUSER_ID, api + + +def post_init_hook(cr, pool): + env = api.Environment(cr, SUPERUSER_ID, {}) + env['letsencrypt'].generate_account_key() diff --git a/letsencrypt/models/__init__.py b/letsencrypt/models/__init__.py new file mode 100644 index 00000000000..c77cf9b6c5e --- /dev/null +++ b/letsencrypt/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import letsencrypt diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py new file mode 100644 index 00000000000..daa262bb1b6 --- /dev/null +++ b/letsencrypt/models/letsencrypt.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# © 2016 Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import os +import logging +import urllib2 +import urlparse +import subprocess +import tempfile +from openerp import _, api, models, exceptions +from openerp.tools import config + + +DEFAULT_KEY_LENGTH = 4096 +_logger = logging.getLogger(__name__) + + +def get_data_dir(): + return os.path.join(config.options.get('data_dir'), 'letsencrypt') + + +def get_challenge_dir(): + return os.path.join(get_data_dir(), 'acme-challenge') + + +class Letsencrypt(models.AbstractModel): + _name = 'letsencrypt' + _description = 'Abstract model providing functions for letsencrypt' + + @api.model + def call_cmdline(self, cmdline, loglevel=logging.INFO, + raise_on_result=True): + process = subprocess.Popen( + cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + if stderr: + _logger.log(loglevel, stderr) + if stdout: + _logger.log(loglevel, stdout) + + if process.returncode: + raise exceptions.Warning( + _('Error calling %s: %d') % (cmdline[0], process.returncode), + ' '.join(cmdline), + ) + + return process.returncode + + @api.model + def generate_account_key(self): + data_dir = get_data_dir() + if not os.path.isdir(data_dir): + os.makedirs(data_dir) + account_key = os.path.join(data_dir, 'account.key') + if not os.path.isfile(account_key): + _logger.info('generating rsa account key') + self.call_cmdline([ + 'openssl', 'genrsa', '-out', account_key, + str(DEFAULT_KEY_LENGTH), + ]) + assert os.path.isfile(account_key), 'failed to create rsa key' + return account_key + + @api.model + def generate_domain_key(self, domain): + domain_key = os.path.join(get_data_dir(), '%s.key' % domain) + if not os.path.isfile(domain_key): + _logger.info('generating rsa domain key for %s', domain) + self.call_cmdline([ + 'openssl', 'genrsa', '-out', domain_key, + str(DEFAULT_KEY_LENGTH), + ]) + return domain_key + + @api.model + def validate_domain(self, domain): + local_domains = [ + 'localhost', 'localhost.localdomain', 'localhost6', + 'localhost6.localdomain6' + ] + + def _ip_is_private(address): + import IPy + try: + ip = IPy.IP(address) + except: + return False + return ip.iptype() == 'PRIVATE' + + if domain in local_domains or _ip_is_private(domain): + raise exceptions.Warning( + _("Let's encrypt doesn't work with private addresses " + "or local domains!")) + + @api.model + def generate_csr(self, domain): + domains = [domain] + i = 0 + while self.env['ir.config_parameter'].get_param( + 'letsencrypt.altname.%d' % i): + domains.append( + self.env['ir.config_parameter'] + .get_param('letsencrypt.altname.%d' % i) + ) + i += 1 + _logger.info('generating csr for %s', domain) + if len(domains) > 1: + _logger.info('with alternative subjects %s', ','.join(domains[1:])) + config = self.env['ir.config_parameter'].get_param( + 'letsencrypt.openssl.cnf', '/etc/ssl/openssl.cnf') + csr = os.path.join(get_data_dir(), '%s.csr' % domain) + with tempfile.NamedTemporaryFile() as cfg: + cfg.write(open(config).read()) + if len(domains) > 1: + cfg.write( + '\n[SAN]\nsubjectAltName=' + + ','.join(map(lambda x: 'DNS:%s' % x, domains)) + '\n') + cfg.file.flush() + cmdline = [ + 'openssl', 'req', '-new', + self.env['ir.config_parameter'].get_param( + 'letsencrypt.openssl.digest', '-sha256'), + '-key', self.generate_domain_key(domain), + '-subj', '/CN=%s' % domain, '-config', cfg.name, + '-out', csr, + ] + if len(domains) > 1: + cmdline.extend([ + '-reqexts', 'SAN', + ]) + self.call_cmdline(cmdline) + return csr + + @api.model + def cron(self): + domain = urlparse.urlparse( + self.env['ir.config_parameter'].get_param( + 'web.base.url', 'localhost')).netloc + self.validate_domain(domain) + account_key = self.generate_account_key() + csr = self.generate_csr(domain) + acme_challenge = get_challenge_dir() + if not os.path.isdir(acme_challenge): + os.makedirs(acme_challenge) + if self.env.context.get('letsencrypt_dry_run'): + crt_text = 'I\'m a test text' + else: # pragma: no cover + from acme_tiny import get_crt, DEFAULT_CA + crt_text = get_crt( + account_key, csr, acme_challenge, log=_logger, CA=DEFAULT_CA) + with open(os.path.join(get_data_dir(), '%s.crt' % domain), 'w')\ + as crt: + crt.write(crt_text) + chain_cert = urllib2.urlopen( + self.env['ir.config_parameter'].get_param( + 'letsencrypt.chain_certificate_address', + 'https://letsencrypt.org/certs/' + 'lets-encrypt-x3-cross-signed.pem') + ) + crt.write(chain_cert.read()) + chain_cert.close() + _logger.info('wrote %s', crt.name) + reload_cmd = self.env['ir.config_parameter'].get_param( + 'letsencrypt.reload_command', False) + if reload_cmd: + _logger.info('reloading webserver...') + self.call_cmdline(['sh', '-c', reload_cmd]) + else: + _logger.info('no command defined for reloading webserver, please ' + 'do it manually in order to apply new certificate') diff --git a/letsencrypt/static/description/icon.png b/letsencrypt/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cf6e022beb0480e7e51e3507b0d5b8a83c8e533b GIT binary patch literal 2157 zcmV-z2$J`SP)sZ}~N3j%@sJCV~r#o2WDGq-9o7W57k7tW0{_3MQfiCbbx0 zF>;C9*d|j6*$gfX*!EV!w(Q3r3(hDETZK-u44q5|xGCt20y2U1$Gzv-AH7RUxj)X~ zo^$U#?fXsoPv6sX-uL~y@AEwG^S12jWG$O24w;kol?hP1Ef$e+L(** zOW^a#U-ctABy~JtfaDv-7REA;w}ENHifnA=NCW$V*e)xu(_<_ZDPRn=$>P?aHY>P5 zgy|VdFET)k3=jjv$N(`wj0_MX1H=F^a;6e5Zldkn0z3d*47^T=^>QbF8Oa@|JD5yA zkD$y0`hX4&vQeVg1qGrpzJn4QGTSa8!F-LeLh5*WM8at0YWnysXQysvb1AccrG+(F z%8ydqrxaz2#@MX8yqSeX1EY;QvDibZJeqQsw({kI0nr_dPnxN8uvT!Cow|e1dfqd_ zM;V^`k>?cdoJ>>pN00r*cxLh| zU@h6VEDje8h(xgwe(GQ}&%owoZDWCF6Qqt4z%rm`Skcd=o}He-h;a#hyaL?rpqGJ)4Shr7jV=u&F>DEegL*Kii9LYIM--5XK3V z`Ch{CC3PYVyi1%p$;D!}seWUwal7|ZgcT01`Uf^uQqS{VKi-#ImMo!%2=xeSAWJGC z#21RnT*1^Vmpw;671F=~KkDFHa!~}8I)02&3!F|f8hf$%d$9l!L4o}lJnrL^u|Ssa ziEnktQ^axw%I|tnzwiO_C!3R@9+vAJ9td3$u5tUi_(9LB)V3=?v&`#F+lh;F5vQV!>^=t}^v zA-so855hFyYG8a0)vAyPe=by?6!wTt8FlD+!8nev0b!dg=HnD;mIVmw;k*u3JP6bl zgOjY54iRv7E_XkC^9Wx zr=wJ2aiOajZF7Jyucq7Xl?NQ--lp7 zQg8YOzD5T#odOx>5s(QP%4WH^Q$JRE6Oy$W5ncyZ9}<|MTPXFlZw7jQyfYx`(P1;2 z7cz{(=`QZnoz2NQ7#z>Jiay7rjsA9dgyTd0!yC?k*sKQL$?<^)2=j1;@(C_X$Ui;k zfPdvWdI6ZmQ%KIIP0k=Jm1XohT8Bs@N4167gt-g2%H=fu6s4OmtyxYcmb)0;Cq0<0 z$6bXGcIr@Ht|P8dZe|9HfVqf+W2S?OBfN>tc8PG%)xuEj=2KuZ#UZ7LlzdC-+2oI> zGVd4`nG6sE#K-^%ko--*S2ukVn>C;_K+4WuL93e_7K-HT*xfu+b@=nt20lO~MtI^z zc1PoWbeVtY_o}8QY<)akpro$5H}^ox-7`~Od?BJ6=s$5SFyD{pFXRi1%9Ya*`8|xR zSE7f^wV7O0d64(^?GJ_zu}Z!HJ|1!_3MO)6aGbj2w1pZB+5WWAL|G>6H8Ma95F-P` z05LK^3=ks&WRzxpO1f&tLEX&>=;H3aj2Q3Vyq7WO{bmA16|XU7n2GC=Yru{yF|Q#$Dn;NM4L z43MExE4w+`9gTy&V}RsCcHH4hGBVi!$xp3s$a>8+&Hw*&lL3+kHB)`xq|B2e@hAcO zKaD{Jaxr%JH_Tmmu1TtimN4i3jle<^W#w1b5)YmGLFT@b3qhKL+NfaXvLr?QMMFVu zl1VNaAVvm=0g^8tqUT0gHj@pILP@Eis%h|hCf78LLAX1}O$zu1m6W5}Mb)ZbpnL*a z2SxTR`OyUgfQcxpjSn$0KnxHg1H=F^GC+(BkWmgq?8M0LIY|Tvh#VUEW7$3w93Y(+ z>;d+SU>YY8o(hl;iQ1G{5#Xf}2;&IwEjl;7>x*&mx9(Ak>JxLYtxF;8MwP jPL=Ft|Hji8jSl?}T@X4bC?)d700000NkvXXu0mjfhQ{G5 literal 0 HcmV?d00001 diff --git a/letsencrypt/tests/__init__.py b/letsencrypt/tests/__init__.py new file mode 100644 index 00000000000..69c38ee80d1 --- /dev/null +++ b/letsencrypt/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_letsencrypt diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py new file mode 100644 index 00000000000..c0dd72a2fe5 --- /dev/null +++ b/letsencrypt/tests/test_letsencrypt.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# © 2016 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from openerp.tests.common import TransactionCase + + +class TestLetsencrypt(TransactionCase): + def test_letsencrypt(self): + from ..hooks import post_init_hook + post_init_hook(self.cr, None) + self.env.ref('letsencrypt.config_parameter_reload').write({ + 'value': '', + }) + self.env['letsencrypt'].with_context(letsencrypt_dry_run=True).cron() From 2ef38f51aa0ef2caac58e62f0f2bc839204b486a Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Thu, 23 Jun 2016 08:48:44 +0200 Subject: [PATCH 02/57] [ADD] disable our cronjob for demo mode fixes #458 --- letsencrypt/__openerp__.py | 1 + letsencrypt/demo/ir_cron.xml | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 letsencrypt/demo/ir_cron.xml diff --git a/letsencrypt/__openerp__.py b/letsencrypt/__openerp__.py index 01457b8073a..626b17e12b2 100644 --- a/letsencrypt/__openerp__.py +++ b/letsencrypt/__openerp__.py @@ -16,6 +16,7 @@ "data": [ "data/ir_config_parameter.xml", "data/ir_cron.xml", + "demo/ir_cron.xml", ], "post_init_hook": 'post_init_hook', "installable": True, diff --git a/letsencrypt/demo/ir_cron.xml b/letsencrypt/demo/ir_cron.xml new file mode 100644 index 00000000000..e4451aa5946 --- /dev/null +++ b/letsencrypt/demo/ir_cron.xml @@ -0,0 +1,8 @@ + + + + + + + + From 77b5d50275c137cf5258550424d3c7a68eafd00b Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Thu, 6 Oct 2016 16:08:19 +0200 Subject: [PATCH 03/57] [MIG] Make modules uninstallable --- letsencrypt/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/__openerp__.py b/letsencrypt/__openerp__.py index 626b17e12b2..95c6b4f96d8 100644 --- a/letsencrypt/__openerp__.py +++ b/letsencrypt/__openerp__.py @@ -19,7 +19,7 @@ "demo/ir_cron.xml", ], "post_init_hook": 'post_init_hook', - "installable": True, + 'installable': False, "external_dependencies": { 'bin': [ 'openssl', From 0691e9786c26b501e8c7624820e2344429307fa7 Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Thu, 6 Oct 2016 16:08:27 +0200 Subject: [PATCH 04/57] [MIG] Rename manifest files --- letsencrypt/{__openerp__.py => __manifest__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename letsencrypt/{__openerp__.py => __manifest__.py} (100%) diff --git a/letsencrypt/__openerp__.py b/letsencrypt/__manifest__.py similarity index 100% rename from letsencrypt/__openerp__.py rename to letsencrypt/__manifest__.py From 945218066bb4057445c89e6b9bd4ff4176fb9f69 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 20 Apr 2017 17:25:11 +0200 Subject: [PATCH 05/57] [10.0][FIX] Make letsencrypt resilient for alternate name removal. (#757) * [FIX] Make letsencrypt resilient for alternate name removal. * [FIX] Do not crash when returning error in letsencrypt cmdline. * [FIX] Restore ordering by name for alternate domains in letsencrypt. Conflicts: letsencrypt/README.rst --- letsencrypt/README.rst | 1 + letsencrypt/models/letsencrypt.py | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst index dfcd02accfb..864682eb506 100644 --- a/letsencrypt/README.rst +++ b/letsencrypt/README.rst @@ -139,6 +139,7 @@ Contributors * Holger Brunn * Antonio Espinosa * Dave Lasley +* Ronald Portier ACME implementation ------------------- diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index daa262bb1b6..225fc15ad30 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -38,13 +38,10 @@ def call_cmdline(self, cmdline, loglevel=logging.INFO, _logger.log(loglevel, stderr) if stdout: _logger.log(loglevel, stdout) - if process.returncode: raise exceptions.Warning( - _('Error calling %s: %d') % (cmdline[0], process.returncode), - ' '.join(cmdline), + _('Error calling %s: %d') % (cmdline[0], process.returncode) ) - return process.returncode @api.model @@ -96,19 +93,19 @@ def _ip_is_private(address): @api.model def generate_csr(self, domain): domains = [domain] - i = 0 - while self.env['ir.config_parameter'].get_param( - 'letsencrypt.altname.%d' % i): - domains.append( - self.env['ir.config_parameter'] - .get_param('letsencrypt.altname.%d' % i) - ) - i += 1 + parameter_model = self.env['ir.config_parameter'] + altnames = parameter_model.search( + [('key', 'like', 'letsencrypt.altname.')], + order='key' + ) + for altname in altnames: + domains.append(altname.value) _logger.info('generating csr for %s', domain) if len(domains) > 1: _logger.info('with alternative subjects %s', ','.join(domains[1:])) - config = self.env['ir.config_parameter'].get_param( - 'letsencrypt.openssl.cnf', '/etc/ssl/openssl.cnf') + config = parameter_model.get_param( + 'letsencrypt.openssl.cnf', '/etc/ssl/openssl.cnf' + ) csr = os.path.join(get_data_dir(), '%s.csr' % domain) with tempfile.NamedTemporaryFile() as cfg: cfg.write(open(config).read()) @@ -119,7 +116,7 @@ def generate_csr(self, domain): cfg.file.flush() cmdline = [ 'openssl', 'req', '-new', - self.env['ir.config_parameter'].get_param( + parameter_model.get_param( 'letsencrypt.openssl.digest', '-sha256'), '-key', self.generate_domain_key(domain), '-subj', '/CN=%s' % domain, '-config', cfg.name, From 4d922e6f9ccf1acfa866ff6e70fb29efe6e74d86 Mon Sep 17 00:00:00 2001 From: Roberto Fichera Date: Fri, 19 May 2017 18:45:56 +0200 Subject: [PATCH 06/57] [10.0][MIG] Migrate letsencrypt --- letsencrypt/__manifest__.py | 4 ++-- letsencrypt/controllers/main.py | 4 ++-- letsencrypt/hooks.py | 2 +- letsencrypt/models/letsencrypt.py | 4 ++-- letsencrypt/tests/test_letsencrypt.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index 95c6b4f96d8..60d0395d615 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { "name": "Let's encrypt", - "version": "9.0.1.0.0", + "version": "10.0.1.0.0", "author": "Therp BV," "Tecnativa," "Odoo Community Association (OCA)", @@ -19,7 +19,7 @@ "demo/ir_cron.xml", ], "post_init_hook": 'post_init_hook', - 'installable': False, + 'installable': True, "external_dependencies": { 'bin': [ 'openssl', diff --git a/letsencrypt/controllers/main.py b/letsencrypt/controllers/main.py index 03b8c4d15a6..07df15d7733 100644 --- a/letsencrypt/controllers/main.py +++ b/letsencrypt/controllers/main.py @@ -3,8 +3,8 @@ # © 2016 Antonio Espinosa # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import os -from openerp import http -from openerp.http import request +from odoo import http +from odoo.http import request from ..models.letsencrypt import get_challenge_dir diff --git a/letsencrypt/hooks.py b/letsencrypt/hooks.py index 3332fa23f71..9a5741c2a68 100644 --- a/letsencrypt/hooks.py +++ b/letsencrypt/hooks.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import SUPERUSER_ID, api +from odoo import SUPERUSER_ID, api def post_init_hook(cr, pool): diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 225fc15ad30..085fdcd34f2 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -8,8 +8,8 @@ import urlparse import subprocess import tempfile -from openerp import _, api, models, exceptions -from openerp.tools import config +from odoo import _, api, models, exceptions +from odoo.tools import config DEFAULT_KEY_LENGTH = 4096 diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index c0dd72a2fe5..3f6002c11bd 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp.tests.common import TransactionCase +from odoo.tests.common import TransactionCase class TestLetsencrypt(TransactionCase): From 2b67b2880366868666dc93b30786bc1c2b3bc47e Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Sun, 29 May 2016 10:44:37 -0400 Subject: [PATCH 07/57] OCA Transbot updated translations from Transifex --- letsencrypt/i18n/am.po | 50 +++++++++++++++++++++++++++++++++ letsencrypt/i18n/ar.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/bg.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/bs.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/ca.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/cs.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/da.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/de.po | 50 +++++++++++++++++++++++++++++++++ letsencrypt/i18n/el_GR.po | 50 +++++++++++++++++++++++++++++++++ letsencrypt/i18n/en_GB.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_AR.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_CL.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_CO.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_CR.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_DO.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_EC.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_ES.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_MX.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_PE.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_PY.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/es_VE.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/et.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/eu.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/fa.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/fi.po | 50 +++++++++++++++++++++++++++++++++ letsencrypt/i18n/fr.po | 54 ++++++++++++++++++++++++++++++++++++ letsencrypt/i18n/fr_CA.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/fr_CH.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/gl.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/gl_ES.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/he.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/hr.po | 52 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/hr_HR.po | 50 +++++++++++++++++++++++++++++++++ letsencrypt/i18n/hu.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/id.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/it.po | 52 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/ja.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/ko.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/lt.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/lt_LT.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/lv.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/mk.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/mn.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/nb.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/nb_NO.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/nl.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/nl_BE.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/nl_NL.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/pl.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/pt.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/pt_BR.po | 50 +++++++++++++++++++++++++++++++++ letsencrypt/i18n/pt_PT.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/ro.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/ru.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/sk.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/sl.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/sr.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/sr@latin.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/sv.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/th.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/tr.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/tr_TR.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/uk.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/vi.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/vi_VN.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/zh_CN.po | 51 ++++++++++++++++++++++++++++++++++ letsencrypt/i18n/zh_TW.po | 51 ++++++++++++++++++++++++++++++++++ 68 files changed, 3467 insertions(+) create mode 100644 letsencrypt/i18n/am.po create mode 100644 letsencrypt/i18n/ar.po create mode 100644 letsencrypt/i18n/bg.po create mode 100644 letsencrypt/i18n/bs.po create mode 100644 letsencrypt/i18n/ca.po create mode 100644 letsencrypt/i18n/cs.po create mode 100644 letsencrypt/i18n/da.po create mode 100644 letsencrypt/i18n/de.po create mode 100644 letsencrypt/i18n/el_GR.po create mode 100644 letsencrypt/i18n/en_GB.po create mode 100644 letsencrypt/i18n/es.po create mode 100644 letsencrypt/i18n/es_AR.po create mode 100644 letsencrypt/i18n/es_CL.po create mode 100644 letsencrypt/i18n/es_CO.po create mode 100644 letsencrypt/i18n/es_CR.po create mode 100644 letsencrypt/i18n/es_DO.po create mode 100644 letsencrypt/i18n/es_EC.po create mode 100644 letsencrypt/i18n/es_ES.po create mode 100644 letsencrypt/i18n/es_MX.po create mode 100644 letsencrypt/i18n/es_PE.po create mode 100644 letsencrypt/i18n/es_PY.po create mode 100644 letsencrypt/i18n/es_VE.po create mode 100644 letsencrypt/i18n/et.po create mode 100644 letsencrypt/i18n/eu.po create mode 100644 letsencrypt/i18n/fa.po create mode 100644 letsencrypt/i18n/fi.po create mode 100644 letsencrypt/i18n/fr.po create mode 100644 letsencrypt/i18n/fr_CA.po create mode 100644 letsencrypt/i18n/fr_CH.po create mode 100644 letsencrypt/i18n/gl.po create mode 100644 letsencrypt/i18n/gl_ES.po create mode 100644 letsencrypt/i18n/he.po create mode 100644 letsencrypt/i18n/hr.po create mode 100644 letsencrypt/i18n/hr_HR.po create mode 100644 letsencrypt/i18n/hu.po create mode 100644 letsencrypt/i18n/id.po create mode 100644 letsencrypt/i18n/it.po create mode 100644 letsencrypt/i18n/ja.po create mode 100644 letsencrypt/i18n/ko.po create mode 100644 letsencrypt/i18n/lt.po create mode 100644 letsencrypt/i18n/lt_LT.po create mode 100644 letsencrypt/i18n/lv.po create mode 100644 letsencrypt/i18n/mk.po create mode 100644 letsencrypt/i18n/mn.po create mode 100644 letsencrypt/i18n/nb.po create mode 100644 letsencrypt/i18n/nb_NO.po create mode 100644 letsencrypt/i18n/nl.po create mode 100644 letsencrypt/i18n/nl_BE.po create mode 100644 letsencrypt/i18n/nl_NL.po create mode 100644 letsencrypt/i18n/pl.po create mode 100644 letsencrypt/i18n/pt.po create mode 100644 letsencrypt/i18n/pt_BR.po create mode 100644 letsencrypt/i18n/pt_PT.po create mode 100644 letsencrypt/i18n/ro.po create mode 100644 letsencrypt/i18n/ru.po create mode 100644 letsencrypt/i18n/sk.po create mode 100644 letsencrypt/i18n/sl.po create mode 100644 letsencrypt/i18n/sr.po create mode 100644 letsencrypt/i18n/sr@latin.po create mode 100644 letsencrypt/i18n/sv.po create mode 100644 letsencrypt/i18n/th.po create mode 100644 letsencrypt/i18n/tr.po create mode 100644 letsencrypt/i18n/tr_TR.po create mode 100644 letsencrypt/i18n/uk.po create mode 100644 letsencrypt/i18n/vi.po create mode 100644 letsencrypt/i18n/vi_VN.po create mode 100644 letsencrypt/i18n/zh_CN.po create mode 100644 letsencrypt/i18n/zh_TW.po diff --git a/letsencrypt/i18n/am.po b/letsencrypt/i18n/am.po new file mode 100644 index 00000000000..eed834f45d0 --- /dev/null +++ b/letsencrypt/i18n/am.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: server-tools (9.0)\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-10 02:53+0000\n" +"PO-Revision-Date: 2016-05-25 16:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: Amharic (http://www.transifex.com/oca/OCA-server-tools-9-0/language/am/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: am\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:44 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:93 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/ar.po b/letsencrypt/i18n/ar.po new file mode 100644 index 00000000000..617f2f19108 --- /dev/null +++ b/letsencrypt/i18n/ar.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Arabic (https://www.transifex.com/oca/teams/23907/ar/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "اسم العرض" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "المعرف" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/bg.po b/letsencrypt/i18n/bg.po new file mode 100644 index 00000000000..b009a1694ef --- /dev/null +++ b/letsencrypt/i18n/bg.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Bulgarian (https://www.transifex.com/oca/teams/23907/bg/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: bg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Име за Показване" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Последно обновено на" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/bs.po b/letsencrypt/i18n/bs.po new file mode 100644 index 00000000000..3c5782e40fd --- /dev/null +++ b/letsencrypt/i18n/bs.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Bosnian (https://www.transifex.com/oca/teams/23907/bs/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: bs\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Prikaži naziv" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/ca.po b/letsencrypt/i18n/ca.po new file mode 100644 index 00000000000..343ded45a44 --- /dev/null +++ b/letsencrypt/i18n/ca.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Veure el nom" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Darrera modificació el" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/cs.po b/letsencrypt/i18n/cs.po new file mode 100644 index 00000000000..847db3afc1f --- /dev/null +++ b/letsencrypt/i18n/cs.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Czech (https://www.transifex.com/oca/teams/23907/cs/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: cs\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Zobrazovaný název" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Naposled upraveno" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/da.po b/letsencrypt/i18n/da.po new file mode 100644 index 00000000000..4e7eed5669b --- /dev/null +++ b/letsencrypt/i18n/da.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Danish (https://www.transifex.com/oca/teams/23907/da/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: da\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Vist navn" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "Id" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Sidst ændret den" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/de.po b/letsencrypt/i18n/de.po new file mode 100644 index 00000000000..ad9df026af4 --- /dev/null +++ b/letsencrypt/i18n/de.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: server-tools (9.0)\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-05-28 02:43+0000\n" +"PO-Revision-Date: 2016-05-25 16:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: German (http://www.transifex.com/oca/OCA-server-tools-9-0/language/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Anzeigename" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:44 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Zuletzt geändert am" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:93 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/el_GR.po b/letsencrypt/i18n/el_GR.po new file mode 100644 index 00000000000..c3090a538a1 --- /dev/null +++ b/letsencrypt/i18n/el_GR.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: server-tools (9.0)\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-10 02:53+0000\n" +"PO-Revision-Date: 2016-05-25 16:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: Greek (Greece) (http://www.transifex.com/oca/OCA-server-tools-9-0/language/el_GR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: el_GR\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:44 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "Κωδικός" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:93 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/en_GB.po b/letsencrypt/i18n/en_GB.po new file mode 100644 index 00000000000..2ef5ba3feb0 --- /dev/null +++ b/letsencrypt/i18n/en_GB.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: English (United Kingdom) (https://www.transifex.com/oca/teams/23907/en_GB/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: en_GB\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Display Name" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Last Modified on" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es.po b/letsencrypt/i18n/es.po new file mode 100644 index 00000000000..b65f3f69448 --- /dev/null +++ b/letsencrypt/i18n/es.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 02:10+0000\n" +"PO-Revision-Date: 2017-12-01 02:10+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nombre a mostrar" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última actualización por" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_AR.po b/letsencrypt/i18n/es_AR.po new file mode 100644 index 00000000000..65bb67a40ff --- /dev/null +++ b/letsencrypt/i18n/es_AR.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Argentina) (https://www.transifex.com/oca/teams/23907/es_AR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_AR\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_CL.po b/letsencrypt/i18n/es_CL.po new file mode 100644 index 00000000000..5e670713db3 --- /dev/null +++ b/letsencrypt/i18n/es_CL.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Chile) (https://www.transifex.com/oca/teams/23907/es_CL/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_CL\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID (identificación)" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_CO.po b/letsencrypt/i18n/es_CO.po new file mode 100644 index 00000000000..c3495f19f2b --- /dev/null +++ b/letsencrypt/i18n/es_CO.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Colombia) (https://www.transifex.com/oca/teams/23907/es_CO/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_CO\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nombre Público" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_CR.po b/letsencrypt/i18n/es_CR.po new file mode 100644 index 00000000000..c4b0424c3d6 --- /dev/null +++ b/letsencrypt/i18n/es_CR.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Costa Rica) (https://www.transifex.com/oca/teams/23907/es_CR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_CR\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_DO.po b/letsencrypt/i18n/es_DO.po new file mode 100644 index 00000000000..d36920884e4 --- /dev/null +++ b/letsencrypt/i18n/es_DO.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Dominican Republic) (https://www.transifex.com/oca/teams/23907/es_DO/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_DO\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_EC.po b/letsencrypt/i18n/es_EC.po new file mode 100644 index 00000000000..ae0f915ccd6 --- /dev/null +++ b/letsencrypt/i18n/es_EC.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Ecuador) (https://www.transifex.com/oca/teams/23907/es_EC/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_EC\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID (identificación)" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_ES.po b/letsencrypt/i18n/es_ES.po new file mode 100644 index 00000000000..67072ba4905 --- /dev/null +++ b/letsencrypt/i18n/es_ES.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/es_ES/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_ES\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nombre para mostrar" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_MX.po b/letsencrypt/i18n/es_MX.po new file mode 100644 index 00000000000..8c207e3a521 --- /dev/null +++ b/letsencrypt/i18n/es_MX.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Mexico) (https://www.transifex.com/oca/teams/23907/es_MX/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_MX\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nombre desplegado" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Ultima modificacion realizada" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_PE.po b/letsencrypt/i18n/es_PE.po new file mode 100644 index 00000000000..7b29d4b230b --- /dev/null +++ b/letsencrypt/i18n/es_PE.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Peru) (https://www.transifex.com/oca/teams/23907/es_PE/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_PE\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nombre a Mostrar" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Ultima Modificación en" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_PY.po b/letsencrypt/i18n/es_PY.po new file mode 100644 index 00000000000..94a7aaf1aa6 --- /dev/null +++ b/letsencrypt/i18n/es_PY.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Paraguay) (https://www.transifex.com/oca/teams/23907/es_PY/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_PY\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/es_VE.po b/letsencrypt/i18n/es_VE.po new file mode 100644 index 00000000000..7e81dacd754 --- /dev/null +++ b/letsencrypt/i18n/es_VE.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Spanish (Venezuela) (https://www.transifex.com/oca/teams/23907/es_VE/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_VE\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Mostrar nombre" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Modificada por última vez" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/et.po b/letsencrypt/i18n/et.po new file mode 100644 index 00000000000..9787518c043 --- /dev/null +++ b/letsencrypt/i18n/et.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Estonian (https://www.transifex.com/oca/teams/23907/et/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Näidatav nimi" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Viimati muudetud" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/eu.po b/letsencrypt/i18n/eu.po new file mode 100644 index 00000000000..6385d58b571 --- /dev/null +++ b/letsencrypt/i18n/eu.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Basque (https://www.transifex.com/oca/teams/23907/eu/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: eu\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Izena erakutsi" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/fa.po b/letsencrypt/i18n/fa.po new file mode 100644 index 00000000000..3a595f1efed --- /dev/null +++ b/letsencrypt/i18n/fa.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Persian (https://www.transifex.com/oca/teams/23907/fa/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fa\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "نام نمایشی" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "شناسه" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "تاریخ آخرین به‌روزرسانی" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/fi.po b/letsencrypt/i18n/fi.po new file mode 100644 index 00000000000..fac54fe59f4 --- /dev/null +++ b/letsencrypt/i18n/fi.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: server-tools (9.0)\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-07-09 10:34+0000\n" +"PO-Revision-Date: 2016-05-25 16:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: Finnish (http://www.transifex.com/oca/OCA-server-tools-9-0/language/fi/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nimi" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:44 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Viimeksi muokattu" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:93 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/fr.po b/letsencrypt/i18n/fr.po new file mode 100644 index 00000000000..87a054ce546 --- /dev/null +++ b/letsencrypt/i18n/fr.po @@ -0,0 +1,54 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +# Quentin THEURET , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 02:10+0000\n" +"PO-Revision-Date: 2017-12-01 02:10+0000\n" +"Last-Translator: Quentin THEURET , 2017\n" +"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "Modèle abstrait fournissant les fonctions pour letsencrypt" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "Erreur lors de l'appel %s : %d" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" +"Let's encrypt ne fonctionne pas avec des adresses privées ou des domaines " +"locaux !" diff --git a/letsencrypt/i18n/fr_CA.po b/letsencrypt/i18n/fr_CA.po new file mode 100644 index 00000000000..96249075858 --- /dev/null +++ b/letsencrypt/i18n/fr_CA.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: French (Canada) (https://www.transifex.com/oca/teams/23907/fr_CA/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr_CA\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Afficher le nom" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "Identifiant" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/fr_CH.po b/letsencrypt/i18n/fr_CH.po new file mode 100644 index 00000000000..f43f082827e --- /dev/null +++ b/letsencrypt/i18n/fr_CH.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: French (Switzerland) (https://www.transifex.com/oca/teams/23907/fr_CH/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr_CH\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/gl.po b/letsencrypt/i18n/gl.po new file mode 100644 index 00000000000..9f055375510 --- /dev/null +++ b/letsencrypt/i18n/gl.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: gl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última modificación" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/gl_ES.po b/letsencrypt/i18n/gl_ES.po new file mode 100644 index 00000000000..52180cec840 --- /dev/null +++ b/letsencrypt/i18n/gl_ES.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Galician (Spain) (https://www.transifex.com/oca/teams/23907/gl_ES/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: gl_ES\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/he.po b/letsencrypt/i18n/he.po new file mode 100644 index 00000000000..4a0b0c06be1 --- /dev/null +++ b/letsencrypt/i18n/he.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Hebrew (https://www.transifex.com/oca/teams/23907/he/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: he\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "השם המוצג" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "מזהה" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "תאריך שינוי אחרון" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/hr.po b/letsencrypt/i18n/hr.po new file mode 100644 index 00000000000..d20e928704e --- /dev/null +++ b/letsencrypt/i18n/hr.po @@ -0,0 +1,52 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +# Bole , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-02 18:41+0000\n" +"PO-Revision-Date: 2018-03-02 18:41+0000\n" +"Last-Translator: Bole , 2018\n" +"Language-Team: Croatian (https://www.transifex.com/oca/teams/23907/hr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: hr\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "Apstraktni model sa metodama za letsencrypt" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Naziv " + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "Greška u pozivu %s: %d" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Zadnje modificirano" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "Let's encrypt ne radi sa privatnim adresama ili lokalnim domenama." diff --git a/letsencrypt/i18n/hr_HR.po b/letsencrypt/i18n/hr_HR.po new file mode 100644 index 00000000000..0d36127a36c --- /dev/null +++ b/letsencrypt/i18n/hr_HR.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: server-tools (9.0)\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-06-09 12:31+0000\n" +"PO-Revision-Date: 2016-05-25 16:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: Croatian (Croatia) (http://www.transifex.com/oca/OCA-server-tools-9-0/language/hr_HR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: hr_HR\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Naziv" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:44 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Zadnje modificirano" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:93 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/hu.po b/letsencrypt/i18n/hu.po new file mode 100644 index 00000000000..6f402dd1d48 --- /dev/null +++ b/letsencrypt/i18n/hu.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Hungarian (https://www.transifex.com/oca/teams/23907/hu/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: hu\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Név megjelenítése" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Utolsó frissítés dátuma" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/id.po b/letsencrypt/i18n/id.po new file mode 100644 index 00000000000..6768761ff35 --- /dev/null +++ b/letsencrypt/i18n/id.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Indonesian (https://www.transifex.com/oca/teams/23907/id/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: id\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nama Tampilan" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Terakhir Dimodifikasi pada" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/it.po b/letsencrypt/i18n/it.po new file mode 100644 index 00000000000..98f284d3107 --- /dev/null +++ b/letsencrypt/i18n/it.po @@ -0,0 +1,52 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +# Paolo Valier , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-06 02:25+0000\n" +"PO-Revision-Date: 2018-01-06 02:25+0000\n" +"Last-Translator: Paolo Valier , 2018\n" +"Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "Modello astratto che fornisce funzioni per letsencrypt" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nome da visualizzare" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "Errore chiamando %s: %d" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "Let's encrypt non funziona con indirizzi privati o domini locali!" diff --git a/letsencrypt/i18n/ja.po b/letsencrypt/i18n/ja.po new file mode 100644 index 00000000000..d9594543422 --- /dev/null +++ b/letsencrypt/i18n/ja.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Japanese (https://www.transifex.com/oca/teams/23907/ja/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ja\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "表示名" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "最終更新日" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/ko.po b/letsencrypt/i18n/ko.po new file mode 100644 index 00000000000..0074a455ba0 --- /dev/null +++ b/letsencrypt/i18n/ko.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Korean (https://www.transifex.com/oca/teams/23907/ko/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ko\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "표시 이름" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "최근 수정" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/lt.po b/letsencrypt/i18n/lt.po new file mode 100644 index 00000000000..b0f042ab9ab --- /dev/null +++ b/letsencrypt/i18n/lt.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Lithuanian (https://www.transifex.com/oca/teams/23907/lt/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: lt\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Vaizduojamas pavadinimas" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Paskutinį kartą keista" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/lt_LT.po b/letsencrypt/i18n/lt_LT.po new file mode 100644 index 00000000000..e49e85f808d --- /dev/null +++ b/letsencrypt/i18n/lt_LT.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Lithuanian (Lithuania) (https://www.transifex.com/oca/teams/23907/lt_LT/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: lt_LT\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/lv.po b/letsencrypt/i18n/lv.po new file mode 100644 index 00000000000..0fe3e0a0ece --- /dev/null +++ b/letsencrypt/i18n/lv.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Latvian (https://www.transifex.com/oca/teams/23907/lv/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: lv\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/mk.po b/letsencrypt/i18n/mk.po new file mode 100644 index 00000000000..86eaec7003f --- /dev/null +++ b/letsencrypt/i18n/mk.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Macedonian (https://www.transifex.com/oca/teams/23907/mk/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: mk\n" +"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Прикажи име" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Последна промена на" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/mn.po b/letsencrypt/i18n/mn.po new file mode 100644 index 00000000000..43016a84a16 --- /dev/null +++ b/letsencrypt/i18n/mn.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Mongolian (https://www.transifex.com/oca/teams/23907/mn/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: mn\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Дэлгэцийн Нэр" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Сүүлийн засвар хийсэн огноо" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/nb.po b/letsencrypt/i18n/nb.po new file mode 100644 index 00000000000..b602a047e99 --- /dev/null +++ b/letsencrypt/i18n/nb.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Norwegian Bokmål (https://www.transifex.com/oca/teams/23907/nb/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: nb\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Visnings navn" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Sist oppdatert " + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/nb_NO.po b/letsencrypt/i18n/nb_NO.po new file mode 100644 index 00000000000..2cfa2ff90ab --- /dev/null +++ b/letsencrypt/i18n/nb_NO.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Norwegian Bokmål (Norway) (https://www.transifex.com/oca/teams/23907/nb_NO/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: nb_NO\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Vis navn" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Sist endret den" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/nl.po b/letsencrypt/i18n/nl.po new file mode 100644 index 00000000000..5aa027e8d10 --- /dev/null +++ b/letsencrypt/i18n/nl.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Dutch (https://www.transifex.com/oca/teams/23907/nl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Te tonen naam" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Laatst bijgewerkt op" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/nl_BE.po b/letsencrypt/i18n/nl_BE.po new file mode 100644 index 00000000000..596879d3aec --- /dev/null +++ b/letsencrypt/i18n/nl_BE.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Dutch (Belgium) (https://www.transifex.com/oca/teams/23907/nl_BE/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: nl_BE\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Schermnaam" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Laatst Aangepast op" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/nl_NL.po b/letsencrypt/i18n/nl_NL.po new file mode 100644 index 00000000000..7ecf33da748 --- /dev/null +++ b/letsencrypt/i18n/nl_NL.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# Peter Hageman , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: Peter Hageman , 2017\n" +"Language-Team: Dutch (Netherlands) (https://www.transifex.com/oca/teams/23907/nl_NL/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: nl_NL\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "weergavenaam" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Laatst gewijzigd op" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/pl.po b/letsencrypt/i18n/pl.po new file mode 100644 index 00000000000..735f2110d50 --- /dev/null +++ b/letsencrypt/i18n/pl.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Polish (https://www.transifex.com/oca/teams/23907/pl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pl\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Wyświetlana nazwa " + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Ostatnio modyfikowano" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/pt.po b/letsencrypt/i18n/pt.po new file mode 100644 index 00000000000..552c81552ce --- /dev/null +++ b/letsencrypt/i18n/pt.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nome" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última Modificação Em" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/pt_BR.po b/letsencrypt/i18n/pt_BR.po new file mode 100644 index 00000000000..7864acbc8be --- /dev/null +++ b/letsencrypt/i18n/pt_BR.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: server-tools (9.0)\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-07-09 10:34+0000\n" +"PO-Revision-Date: 2016-05-25 16:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/oca/OCA-server-tools-9-0/language/pt_BR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nome para Mostrar" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:44 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "Identificação" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última atualização em" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:93 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/pt_PT.po b/letsencrypt/i18n/pt_PT.po new file mode 100644 index 00000000000..39fdd34d9a6 --- /dev/null +++ b/letsencrypt/i18n/pt_PT.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/teams/23907/pt_PT/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pt_PT\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nome a Apresentar" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Última Modificação Em" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/ro.po b/letsencrypt/i18n/ro.po new file mode 100644 index 00000000000..fe6cd018192 --- /dev/null +++ b/letsencrypt/i18n/ro.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Romanian (https://www.transifex.com/oca/teams/23907/ro/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ro\n" +"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Nume Afişat" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Ultima actualizare în" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/ru.po b/letsencrypt/i18n/ru.po new file mode 100644 index 00000000000..7018c3dd293 --- /dev/null +++ b/letsencrypt/i18n/ru.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Russian (https://www.transifex.com/oca/teams/23907/ru/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ru\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/sk.po b/letsencrypt/i18n/sk.po new file mode 100644 index 00000000000..f5f50459470 --- /dev/null +++ b/letsencrypt/i18n/sk.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Slovak (https://www.transifex.com/oca/teams/23907/sk/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sk\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Zobraziť meno" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Posledná modifikácia" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/sl.po b/letsencrypt/i18n/sl.po new file mode 100644 index 00000000000..179a156b0d4 --- /dev/null +++ b/letsencrypt/i18n/sl.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 02:10+0000\n" +"PO-Revision-Date: 2017-12-01 02:10+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sl\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Prikazni naziv" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Zadnjič spremenjeno" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/sr.po b/letsencrypt/i18n/sr.po new file mode 100644 index 00000000000..e7294695d42 --- /dev/null +++ b/letsencrypt/i18n/sr.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Serbian (https://www.transifex.com/oca/teams/23907/sr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sr\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/sr@latin.po b/letsencrypt/i18n/sr@latin.po new file mode 100644 index 00000000000..f750837d10a --- /dev/null +++ b/letsencrypt/i18n/sr@latin.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Serbian (Latin) (https://www.transifex.com/oca/teams/23907/sr@latin/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sr@latin\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Ime za prikaz" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Zadnja izmjena" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/sv.po b/letsencrypt/i18n/sv.po new file mode 100644 index 00000000000..0e2caac4110 --- /dev/null +++ b/letsencrypt/i18n/sv.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Swedish (https://www.transifex.com/oca/teams/23907/sv/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Visa namn" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Senast redigerad" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/th.po b/letsencrypt/i18n/th.po new file mode 100644 index 00000000000..b63d3d64a2a --- /dev/null +++ b/letsencrypt/i18n/th.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Thai (https://www.transifex.com/oca/teams/23907/th/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: th\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "ชื่อที่ใช้แสดง" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "รหัส" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "แก้ไขครั้งสุดท้ายเมื่อ" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/tr.po b/letsencrypt/i18n/tr.po new file mode 100644 index 00000000000..4f20a31f4aa --- /dev/null +++ b/letsencrypt/i18n/tr.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Görünen İsim" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Son değişiklik" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/tr_TR.po b/letsencrypt/i18n/tr_TR.po new file mode 100644 index 00000000000..5a1dbd80c9e --- /dev/null +++ b/letsencrypt/i18n/tr_TR.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Turkish (Turkey) (https://www.transifex.com/oca/teams/23907/tr_TR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: tr_TR\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Görünen ad" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "Kimlik" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "En son güncelleme tarihi" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/uk.po b/letsencrypt/i18n/uk.po new file mode 100644 index 00000000000..48459c47a61 --- /dev/null +++ b/letsencrypt/i18n/uk.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Ukrainian (https://www.transifex.com/oca/teams/23907/uk/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: uk\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Назва для відображення" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Остання модифікація" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/vi.po b/letsencrypt/i18n/vi.po new file mode 100644 index 00000000000..afea7f10e1e --- /dev/null +++ b/letsencrypt/i18n/vi.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Vietnamese (https://www.transifex.com/oca/teams/23907/vi/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: vi\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "Tên hiển thị" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "Sửa lần cuối vào" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/vi_VN.po b/letsencrypt/i18n/vi_VN.po new file mode 100644 index 00000000000..0333fc1738b --- /dev/null +++ b/letsencrypt/i18n/vi_VN.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Vietnamese (Viet Nam) (https://www.transifex.com/oca/teams/23907/vi_VN/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: vi_VN\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/zh_CN.po b/letsencrypt/i18n/zh_CN.po new file mode 100644 index 00000000000..8d5b0687992 --- /dev/null +++ b/letsencrypt/i18n/zh_CN.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Chinese (China) (https://www.transifex.com/oca/teams/23907/zh_CN/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "显示名称" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "错误,调用%s: %d" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "ID" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "最后修改时间" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" diff --git a/letsencrypt/i18n/zh_TW.po b/letsencrypt/i18n/zh_TW.po new file mode 100644 index 00000000000..220cd12c862 --- /dev/null +++ b/letsencrypt/i18n/zh_TW.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-08-01 02:44+0000\n" +"PO-Revision-Date: 2017-08-01 02:44+0000\n" +"Last-Translator: OCA Transbot , 2017\n" +"Language-Team: Chinese (Taiwan) (https://www.transifex.com/oca/teams/23907/zh_TW/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "顯示名稱" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "編號" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "最後修改:" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" From 4aab44f5b60e429def8e78b9e709dca57171b610 Mon Sep 17 00:00:00 2001 From: "Ignacio Ibeas - Acysos S.L" Date: Sun, 13 May 2018 12:08:30 +0200 Subject: [PATCH 08/57] [MIG] letsencrypt: migration to 11.0 --- letsencrypt/README.rst | 1 + letsencrypt/__init__.py | 1 - letsencrypt/__manifest__.py | 4 ++-- letsencrypt/controllers/__init__.py | 1 - letsencrypt/controllers/main.py | 4 ++-- letsencrypt/data/ir_config_parameter.xml | 1 - letsencrypt/data/ir_cron.xml | 8 ++++---- letsencrypt/hooks.py | 1 - letsencrypt/models/__init__.py | 1 - letsencrypt/models/letsencrypt.py | 20 ++++++++++---------- letsencrypt/tests/__init__.py | 1 - letsencrypt/tests/test_letsencrypt.py | 1 - 12 files changed, 19 insertions(+), 25 deletions(-) diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst index 864682eb506..b1b419bcd28 100644 --- a/letsencrypt/README.rst +++ b/letsencrypt/README.rst @@ -140,6 +140,7 @@ Contributors * Antonio Espinosa * Dave Lasley * Ronald Portier +* Ignacio Ibeas ACME implementation ------------------- diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 5a561ec1084..a89dc31af16 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import models diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index 60d0395d615..5c2984289b2 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { "name": "Let's encrypt", - "version": "10.0.1.0.0", + "version": "11.0.1.0.0", "author": "Therp BV," "Tecnativa," + "Acysos S.L," "Odoo Community Association (OCA)", "license": "AGPL-3", "category": "Hidden/Dependency", diff --git a/letsencrypt/controllers/__init__.py b/letsencrypt/controllers/__init__.py index b0d7e8a3843..685eb0d7120 100644 --- a/letsencrypt/controllers/__init__.py +++ b/letsencrypt/controllers/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import main diff --git a/letsencrypt/controllers/main.py b/letsencrypt/controllers/main.py index 07df15d7733..9d9599117e3 100644 --- a/letsencrypt/controllers/main.py +++ b/letsencrypt/controllers/main.py @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # © 2016 Antonio Espinosa +# © 2018 Ignacio Ibeas # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import os from odoo import http @@ -12,7 +12,7 @@ class Letsencrypt(http.Controller): @http.route('/.well-known/acme-challenge/', auth='none') def acme_challenge(self, filename): try: - with file(os.path.join(get_challenge_dir(), filename)) as key: + with open(os.path.join(get_challenge_dir(), filename)) as key: return key.read() except IOError: pass diff --git a/letsencrypt/data/ir_config_parameter.xml b/letsencrypt/data/ir_config_parameter.xml index eb3c3f54b8d..13f91f2edbc 100644 --- a/letsencrypt/data/ir_config_parameter.xml +++ b/letsencrypt/data/ir_config_parameter.xml @@ -4,7 +4,6 @@ letsencrypt.reload_command sudo /usr/sbin/service nginx reload - diff --git a/letsencrypt/data/ir_cron.xml b/letsencrypt/data/ir_cron.xml index 1b43f86092c..f2f065cd9e3 100644 --- a/letsencrypt/data/ir_cron.xml +++ b/letsencrypt/data/ir_cron.xml @@ -3,12 +3,12 @@ Update letsencrypt certificates - weeks + + code + model.cron() 11 + weeks -1 - letsencrypt - cron - 2016-01-01 diff --git a/letsencrypt/hooks.py b/letsencrypt/hooks.py index 9a5741c2a68..88ac2e8892e 100644 --- a/letsencrypt/hooks.py +++ b/letsencrypt/hooks.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import SUPERUSER_ID, api diff --git a/letsencrypt/models/__init__.py b/letsencrypt/models/__init__.py index c77cf9b6c5e..953f1c5d0aa 100644 --- a/letsencrypt/models/__init__.py +++ b/letsencrypt/models/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import letsencrypt diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 085fdcd34f2..8899f135a8c 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # © 2016 Antonio Espinosa +# © 2018 Ignacio Ibeas # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import os import logging -import urllib2 -import urlparse +import urllib.request +import urllib.parse import subprocess import tempfile from odoo import _, api, models, exceptions @@ -81,7 +81,7 @@ def _ip_is_private(address): import IPy try: ip = IPy.IP(address) - except: + except Exception: return False return ip.iptype() == 'PRIVATE' @@ -107,12 +107,12 @@ def generate_csr(self, domain): 'letsencrypt.openssl.cnf', '/etc/ssl/openssl.cnf' ) csr = os.path.join(get_data_dir(), '%s.csr' % domain) - with tempfile.NamedTemporaryFile() as cfg: + with tempfile.NamedTemporaryFile(mode='wt') as cfg: cfg.write(open(config).read()) if len(domains) > 1: cfg.write( '\n[SAN]\nsubjectAltName=' + - ','.join(map(lambda x: 'DNS:%s' % x, domains)) + '\n') + ','.join(['DNS:%s' % x for x in domains]) + '\n') cfg.file.flush() cmdline = [ 'openssl', 'req', '-new', @@ -131,7 +131,7 @@ def generate_csr(self, domain): @api.model def cron(self): - domain = urlparse.urlparse( + domain = urllib.parse.urlparse( self.env['ir.config_parameter'].get_param( 'web.base.url', 'localhost')).netloc self.validate_domain(domain) @@ -149,16 +149,16 @@ def cron(self): with open(os.path.join(get_data_dir(), '%s.crt' % domain), 'w')\ as crt: crt.write(crt_text) - chain_cert = urllib2.urlopen( + chain_cert = urllib.request.urlopen( self.env['ir.config_parameter'].get_param( 'letsencrypt.chain_certificate_address', 'https://letsencrypt.org/certs/' 'lets-encrypt-x3-cross-signed.pem') ) - crt.write(chain_cert.read()) + crt.write(str(chain_cert.read())) chain_cert.close() _logger.info('wrote %s', crt.name) - reload_cmd = self.env['ir.config_parameter'].get_param( + reload_cmd = self.env['ir.config_parameter'].sudo().get_param( 'letsencrypt.reload_command', False) if reload_cmd: _logger.info('reloading webserver...') diff --git a/letsencrypt/tests/__init__.py b/letsencrypt/tests/__init__.py index 69c38ee80d1..c5eb768cad8 100644 --- a/letsencrypt/tests/__init__.py +++ b/letsencrypt/tests/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import test_letsencrypt diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index 3f6002c11bd..60616d9af7e 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo.tests.common import TransactionCase From 83142ee451384b983d25acb9d31688896fb279fe Mon Sep 17 00:00:00 2001 From: oca-travis Date: Sun, 17 Jun 2018 20:50:52 +0000 Subject: [PATCH 09/57] [UPD] Update letsencrypt.pot --- letsencrypt/i18n/am.po | 18 ++++++++--- letsencrypt/i18n/ar.po | 14 +++++++-- letsencrypt/i18n/bg.po | 11 +++++-- letsencrypt/i18n/bs.po | 14 +++++++-- letsencrypt/i18n/ca.po | 11 +++++-- letsencrypt/i18n/cs.po | 11 +++++-- letsencrypt/i18n/da.po | 11 +++++-- letsencrypt/i18n/de.po | 18 ++++++++--- letsencrypt/i18n/el_GR.po | 18 ++++++++--- letsencrypt/i18n/en_GB.po | 14 +++++++-- letsencrypt/i18n/es.po | 11 +++++-- letsencrypt/i18n/es_AR.po | 14 +++++++-- letsencrypt/i18n/es_CL.po | 14 +++++++-- letsencrypt/i18n/es_CO.po | 14 +++++++-- letsencrypt/i18n/es_CR.po | 14 +++++++-- letsencrypt/i18n/es_DO.po | 14 +++++++-- letsencrypt/i18n/es_EC.po | 14 +++++++-- letsencrypt/i18n/es_ES.po | 14 +++++++-- letsencrypt/i18n/es_MX.po | 14 +++++++-- letsencrypt/i18n/es_PE.po | 14 +++++++-- letsencrypt/i18n/es_PY.po | 14 +++++++-- letsencrypt/i18n/es_VE.po | 14 +++++++-- letsencrypt/i18n/et.po | 11 +++++-- letsencrypt/i18n/eu.po | 11 +++++-- letsencrypt/i18n/fa.po | 11 +++++-- letsencrypt/i18n/fi.po | 18 ++++++++--- letsencrypt/i18n/fr.po | 11 +++++-- letsencrypt/i18n/fr_CA.po | 14 +++++++-- letsencrypt/i18n/fr_CH.po | 14 +++++++-- letsencrypt/i18n/gl.po | 11 +++++-- letsencrypt/i18n/gl_ES.po | 14 +++++++-- letsencrypt/i18n/he.po | 11 +++++-- letsencrypt/i18n/hr.po | 14 +++++++-- letsencrypt/i18n/hr_HR.po | 21 +++++++++---- letsencrypt/i18n/hu.po | 11 +++++-- letsencrypt/i18n/id.po | 11 +++++-- letsencrypt/i18n/it.po | 11 +++++-- letsencrypt/i18n/ja.po | 11 +++++-- letsencrypt/i18n/ko.po | 11 +++++-- letsencrypt/i18n/letsencrypt.pot | 54 ++++++++++++++++++++++++++++++++ letsencrypt/i18n/lt.po | 14 +++++++-- letsencrypt/i18n/lt_LT.po | 17 +++++++--- letsencrypt/i18n/lv.po | 14 +++++++-- letsencrypt/i18n/mk.po | 11 +++++-- letsencrypt/i18n/mn.po | 11 +++++-- letsencrypt/i18n/nb.po | 14 +++++++-- letsencrypt/i18n/nb_NO.po | 14 +++++++-- letsencrypt/i18n/nl.po | 11 +++++-- letsencrypt/i18n/nl_BE.po | 14 +++++++-- letsencrypt/i18n/nl_NL.po | 14 +++++++-- letsencrypt/i18n/pl.po | 15 +++++++-- letsencrypt/i18n/pt.po | 11 +++++-- letsencrypt/i18n/pt_BR.po | 18 ++++++++--- letsencrypt/i18n/pt_PT.po | 14 +++++++-- letsencrypt/i18n/ro.po | 14 +++++++-- letsencrypt/i18n/ru.po | 15 +++++++-- letsencrypt/i18n/sk.po | 11 +++++-- letsencrypt/i18n/sl.po | 14 +++++++-- letsencrypt/i18n/sr.po | 14 +++++++-- letsencrypt/i18n/sr@latin.po | 17 +++++++--- letsencrypt/i18n/sv.po | 11 +++++-- letsencrypt/i18n/th.po | 11 +++++-- letsencrypt/i18n/tr.po | 11 +++++-- letsencrypt/i18n/tr_TR.po | 14 +++++++-- letsencrypt/i18n/uk.po | 14 +++++++-- letsencrypt/i18n/vi.po | 11 +++++-- letsencrypt/i18n/vi_VN.po | 14 +++++++-- letsencrypt/i18n/zh_CN.po | 14 +++++++-- letsencrypt/i18n/zh_TW.po | 14 +++++++-- 69 files changed, 772 insertions(+), 194 deletions(-) create mode 100644 letsencrypt/i18n/letsencrypt.pot diff --git a/letsencrypt/i18n/am.po b/letsencrypt/i18n/am.po index eed834f45d0..77684e85904 100644 --- a/letsencrypt/i18n/am.po +++ b/letsencrypt/i18n/am.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: msgid "" msgstr "" @@ -10,11 +10,12 @@ msgstr "" "POT-Creation-Date: 2016-09-10 02:53+0000\n" "PO-Revision-Date: 2016-05-25 16:43+0000\n" "Last-Translator: <>\n" -"Language-Team: Amharic (http://www.transifex.com/oca/OCA-server-tools-9-0/language/am/)\n" +"Language-Team: Amharic (http://www.transifex.com/oca/OCA-server-tools-9-0/" +"language/am/)\n" +"Language: am\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: am\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: letsencrypt @@ -28,7 +29,7 @@ msgid "Display Name" msgstr "" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:44 +#: code:addons/letsencrypt/models/letsencrypt.py:43 #, python-format msgid "Error calling %s: %d" msgstr "" @@ -44,7 +45,14 @@ msgid "Last Modified on" msgstr "" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:93 +#: code:addons/letsencrypt/models/letsencrypt.py:90 #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/ar.po b/letsencrypt/i18n/ar.po index 617f2f19108..c65efff6f0f 100644 --- a/letsencrypt/i18n/ar.po +++ b/letsencrypt/i18n/ar.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Arabic (https://www.transifex.com/oca/teams/23907/ar/)\n" +"Language: ar\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ar\n" -"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +50,10 @@ msgstr "آخر تعديل في" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/bg.po b/letsencrypt/i18n/bg.po index b009a1694ef..c5ca3ea2bd5 100644 --- a/letsencrypt/i18n/bg.po +++ b/letsencrypt/i18n/bg.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Bulgarian (https://www.transifex.com/oca/teams/23907/bg/)\n" +"Language: bg\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: bg\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Последно обновено на" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/bs.po b/letsencrypt/i18n/bs.po index 3c5782e40fd..fee14ce0d3e 100644 --- a/letsencrypt/i18n/bs.po +++ b/letsencrypt/i18n/bs.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Bosnian (https://www.transifex.com/oca/teams/23907/bs/)\n" +"Language: bs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: bs\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +50,10 @@ msgstr "Zadnje mijenjano" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/ca.po b/letsencrypt/i18n/ca.po index 343ded45a44..adaf4337f55 100644 --- a/letsencrypt/i18n/ca.po +++ b/letsencrypt/i18n/ca.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n" +"Language: ca\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ca\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Darrera modificació el" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/cs.po b/letsencrypt/i18n/cs.po index 847db3afc1f..799c3119c14 100644 --- a/letsencrypt/i18n/cs.po +++ b/letsencrypt/i18n/cs.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Czech (https://www.transifex.com/oca/teams/23907/cs/)\n" +"Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: cs\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Naposled upraveno" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/da.po b/letsencrypt/i18n/da.po index 4e7eed5669b..40fa752c8a1 100644 --- a/letsencrypt/i18n/da.po +++ b/letsencrypt/i18n/da.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Danish (https://www.transifex.com/oca/teams/23907/da/)\n" +"Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: da\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Sidst ændret den" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/de.po b/letsencrypt/i18n/de.po index ad9df026af4..17def380d45 100644 --- a/letsencrypt/i18n/de.po +++ b/letsencrypt/i18n/de.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: msgid "" msgstr "" @@ -10,11 +10,12 @@ msgstr "" "POT-Creation-Date: 2016-05-28 02:43+0000\n" "PO-Revision-Date: 2016-05-25 16:43+0000\n" "Last-Translator: <>\n" -"Language-Team: German (http://www.transifex.com/oca/OCA-server-tools-9-0/language/de/)\n" +"Language-Team: German (http://www.transifex.com/oca/OCA-server-tools-9-0/" +"language/de/)\n" +"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -28,7 +29,7 @@ msgid "Display Name" msgstr "Anzeigename" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:44 +#: code:addons/letsencrypt/models/letsencrypt.py:43 #, python-format msgid "Error calling %s: %d" msgstr "" @@ -44,7 +45,14 @@ msgid "Last Modified on" msgstr "Zuletzt geändert am" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:93 +#: code:addons/letsencrypt/models/letsencrypt.py:90 #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/el_GR.po b/letsencrypt/i18n/el_GR.po index c3090a538a1..042908e0b46 100644 --- a/letsencrypt/i18n/el_GR.po +++ b/letsencrypt/i18n/el_GR.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: msgid "" msgstr "" @@ -10,11 +10,12 @@ msgstr "" "POT-Creation-Date: 2016-09-10 02:53+0000\n" "PO-Revision-Date: 2016-05-25 16:43+0000\n" "Last-Translator: <>\n" -"Language-Team: Greek (Greece) (http://www.transifex.com/oca/OCA-server-tools-9-0/language/el_GR/)\n" +"Language-Team: Greek (Greece) (http://www.transifex.com/oca/OCA-server-" +"tools-9-0/language/el_GR/)\n" +"Language: el_GR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: el_GR\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -28,7 +29,7 @@ msgid "Display Name" msgstr "" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:44 +#: code:addons/letsencrypt/models/letsencrypt.py:43 #, python-format msgid "Error calling %s: %d" msgstr "" @@ -44,7 +45,14 @@ msgid "Last Modified on" msgstr "" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:93 +#: code:addons/letsencrypt/models/letsencrypt.py:90 #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/en_GB.po b/letsencrypt/i18n/en_GB.po index 2ef5ba3feb0..9bc278e02b3 100644 --- a/letsencrypt/i18n/en_GB.po +++ b/letsencrypt/i18n/en_GB.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: English (United Kingdom) (https://www.transifex.com/oca/teams/23907/en_GB/)\n" +"Language-Team: English (United Kingdom) (https://www.transifex.com/oca/" +"teams/23907/en_GB/)\n" +"Language: en_GB\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: en_GB\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Last Modified on" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es.po b/letsencrypt/i18n/es.po index b65f3f69448..f7064875ffe 100644 --- a/letsencrypt/i18n/es.po +++ b/letsencrypt/i18n/es.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-12-01 02:10+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Última actualización por" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_AR.po b/letsencrypt/i18n/es_AR.po index 65bb67a40ff..87847f40594 100644 --- a/letsencrypt/i18n/es_AR.po +++ b/letsencrypt/i18n/es_AR.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Argentina) (https://www.transifex.com/oca/teams/23907/es_AR/)\n" +"Language-Team: Spanish (Argentina) (https://www.transifex.com/oca/" +"teams/23907/es_AR/)\n" +"Language: es_AR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_AR\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Última modificación en" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_CL.po b/letsencrypt/i18n/es_CL.po index 5e670713db3..0f97ac7d2db 100644 --- a/letsencrypt/i18n/es_CL.po +++ b/letsencrypt/i18n/es_CL.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Chile) (https://www.transifex.com/oca/teams/23907/es_CL/)\n" +"Language-Team: Spanish (Chile) (https://www.transifex.com/oca/teams/23907/" +"es_CL/)\n" +"Language: es_CL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_CL\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Última modificación en" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_CO.po b/letsencrypt/i18n/es_CO.po index c3495f19f2b..59f4abe546a 100644 --- a/letsencrypt/i18n/es_CO.po +++ b/letsencrypt/i18n/es_CO.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Colombia) (https://www.transifex.com/oca/teams/23907/es_CO/)\n" +"Language-Team: Spanish (Colombia) (https://www.transifex.com/oca/teams/23907/" +"es_CO/)\n" +"Language: es_CO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_CO\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Última Modificación el" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_CR.po b/letsencrypt/i18n/es_CR.po index c4b0424c3d6..d9072230dd4 100644 --- a/letsencrypt/i18n/es_CR.po +++ b/letsencrypt/i18n/es_CR.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Costa Rica) (https://www.transifex.com/oca/teams/23907/es_CR/)\n" +"Language-Team: Spanish (Costa Rica) (https://www.transifex.com/oca/" +"teams/23907/es_CR/)\n" +"Language: es_CR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_CR\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_DO.po b/letsencrypt/i18n/es_DO.po index d36920884e4..5b3cd551d3c 100644 --- a/letsencrypt/i18n/es_DO.po +++ b/letsencrypt/i18n/es_DO.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Dominican Republic) (https://www.transifex.com/oca/teams/23907/es_DO/)\n" +"Language-Team: Spanish (Dominican Republic) (https://www.transifex.com/oca/" +"teams/23907/es_DO/)\n" +"Language: es_DO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_DO\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Última modificación en" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_EC.po b/letsencrypt/i18n/es_EC.po index ae0f915ccd6..3533e0a157d 100644 --- a/letsencrypt/i18n/es_EC.po +++ b/letsencrypt/i18n/es_EC.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Ecuador) (https://www.transifex.com/oca/teams/23907/es_EC/)\n" +"Language-Team: Spanish (Ecuador) (https://www.transifex.com/oca/teams/23907/" +"es_EC/)\n" +"Language: es_EC\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_EC\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Última modificación en" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_ES.po b/letsencrypt/i18n/es_ES.po index 67072ba4905..f67b61fe375 100644 --- a/letsencrypt/i18n/es_ES.po +++ b/letsencrypt/i18n/es_ES.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/es_ES/)\n" +"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/" +"es_ES/)\n" +"Language: es_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_ES\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Última modificación en" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_MX.po b/letsencrypt/i18n/es_MX.po index 8c207e3a521..36014e9b3db 100644 --- a/letsencrypt/i18n/es_MX.po +++ b/letsencrypt/i18n/es_MX.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Mexico) (https://www.transifex.com/oca/teams/23907/es_MX/)\n" +"Language-Team: Spanish (Mexico) (https://www.transifex.com/oca/teams/23907/" +"es_MX/)\n" +"Language: es_MX\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_MX\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Ultima modificacion realizada" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_PE.po b/letsencrypt/i18n/es_PE.po index 7b29d4b230b..cbd97717a02 100644 --- a/letsencrypt/i18n/es_PE.po +++ b/letsencrypt/i18n/es_PE.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Peru) (https://www.transifex.com/oca/teams/23907/es_PE/)\n" +"Language-Team: Spanish (Peru) (https://www.transifex.com/oca/teams/23907/" +"es_PE/)\n" +"Language: es_PE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_PE\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Ultima Modificación en" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_PY.po b/letsencrypt/i18n/es_PY.po index 94a7aaf1aa6..93f5c5e2cd2 100644 --- a/letsencrypt/i18n/es_PY.po +++ b/letsencrypt/i18n/es_PY.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Paraguay) (https://www.transifex.com/oca/teams/23907/es_PY/)\n" +"Language-Team: Spanish (Paraguay) (https://www.transifex.com/oca/teams/23907/" +"es_PY/)\n" +"Language: es_PY\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_PY\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/es_VE.po b/letsencrypt/i18n/es_VE.po index 7e81dacd754..63248d75431 100644 --- a/letsencrypt/i18n/es_VE.po +++ b/letsencrypt/i18n/es_VE.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Spanish (Venezuela) (https://www.transifex.com/oca/teams/23907/es_VE/)\n" +"Language-Team: Spanish (Venezuela) (https://www.transifex.com/oca/" +"teams/23907/es_VE/)\n" +"Language: es_VE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_VE\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Modificada por última vez" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/et.po b/letsencrypt/i18n/et.po index 9787518c043..0643e238a9b 100644 --- a/letsencrypt/i18n/et.po +++ b/letsencrypt/i18n/et.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Estonian (https://www.transifex.com/oca/teams/23907/et/)\n" +"Language: et\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: et\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Viimati muudetud" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/eu.po b/letsencrypt/i18n/eu.po index 6385d58b571..e4a56cc746a 100644 --- a/letsencrypt/i18n/eu.po +++ b/letsencrypt/i18n/eu.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Basque (https://www.transifex.com/oca/teams/23907/eu/)\n" +"Language: eu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: eu\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/fa.po b/letsencrypt/i18n/fa.po index 3a595f1efed..e533c68b38c 100644 --- a/letsencrypt/i18n/fa.po +++ b/letsencrypt/i18n/fa.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Persian (https://www.transifex.com/oca/teams/23907/fa/)\n" +"Language: fa\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fa\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "تاریخ آخرین به‌روزرسانی" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/fi.po b/letsencrypt/i18n/fi.po index fac54fe59f4..737c5a164f9 100644 --- a/letsencrypt/i18n/fi.po +++ b/letsencrypt/i18n/fi.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: msgid "" msgstr "" @@ -10,11 +10,12 @@ msgstr "" "POT-Creation-Date: 2016-07-09 10:34+0000\n" "PO-Revision-Date: 2016-05-25 16:43+0000\n" "Last-Translator: <>\n" -"Language-Team: Finnish (http://www.transifex.com/oca/OCA-server-tools-9-0/language/fi/)\n" +"Language-Team: Finnish (http://www.transifex.com/oca/OCA-server-tools-9-0/" +"language/fi/)\n" +"Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fi\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -28,7 +29,7 @@ msgid "Display Name" msgstr "Nimi" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:44 +#: code:addons/letsencrypt/models/letsencrypt.py:43 #, python-format msgid "Error calling %s: %d" msgstr "" @@ -44,7 +45,14 @@ msgid "Last Modified on" msgstr "Viimeksi muokattu" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:93 +#: code:addons/letsencrypt/models/letsencrypt.py:90 #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/fr.po b/letsencrypt/i18n/fr.po index 87a054ce546..953fca321a3 100644 --- a/letsencrypt/i18n/fr.po +++ b/letsencrypt/i18n/fr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 # Quentin THEURET , 2017 @@ -13,10 +13,10 @@ msgstr "" "PO-Revision-Date: 2017-12-01 02:10+0000\n" "Last-Translator: Quentin THEURET , 2017\n" "Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: letsencrypt @@ -52,3 +52,10 @@ msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" "Let's encrypt ne fonctionne pas avec des adresses privées ou des domaines " "locaux !" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/fr_CA.po b/letsencrypt/i18n/fr_CA.po index 96249075858..978f41ff968 100644 --- a/letsencrypt/i18n/fr_CA.po +++ b/letsencrypt/i18n/fr_CA.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: French (Canada) (https://www.transifex.com/oca/teams/23907/fr_CA/)\n" +"Language-Team: French (Canada) (https://www.transifex.com/oca/teams/23907/" +"fr_CA/)\n" +"Language: fr_CA\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fr_CA\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/fr_CH.po b/letsencrypt/i18n/fr_CH.po index f43f082827e..3caa6481665 100644 --- a/letsencrypt/i18n/fr_CH.po +++ b/letsencrypt/i18n/fr_CH.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: French (Switzerland) (https://www.transifex.com/oca/teams/23907/fr_CH/)\n" +"Language-Team: French (Switzerland) (https://www.transifex.com/oca/" +"teams/23907/fr_CH/)\n" +"Language: fr_CH\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fr_CH\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Dernière modification le" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/gl.po b/letsencrypt/i18n/gl.po index 9f055375510..c8b16b0e444 100644 --- a/letsencrypt/i18n/gl.po +++ b/letsencrypt/i18n/gl.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n" +"Language: gl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: gl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Última modificación" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/gl_ES.po b/letsencrypt/i18n/gl_ES.po index 52180cec840..293378cfd37 100644 --- a/letsencrypt/i18n/gl_ES.po +++ b/letsencrypt/i18n/gl_ES.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Galician (Spain) (https://www.transifex.com/oca/teams/23907/gl_ES/)\n" +"Language-Team: Galician (Spain) (https://www.transifex.com/oca/teams/23907/" +"gl_ES/)\n" +"Language: gl_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: gl_ES\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/he.po b/letsencrypt/i18n/he.po index 4a0b0c06be1..1e26a55775b 100644 --- a/letsencrypt/i18n/he.po +++ b/letsencrypt/i18n/he.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Hebrew (https://www.transifex.com/oca/teams/23907/he/)\n" +"Language: he\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: he\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "תאריך שינוי אחרון" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/hr.po b/letsencrypt/i18n/hr.po index d20e928704e..b30a0335b6b 100644 --- a/letsencrypt/i18n/hr.po +++ b/letsencrypt/i18n/hr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 # Bole , 2018 @@ -13,11 +13,12 @@ msgstr "" "PO-Revision-Date: 2018-03-02 18:41+0000\n" "Last-Translator: Bole , 2018\n" "Language-Team: Croatian (https://www.transifex.com/oca/teams/23907/hr/)\n" +"Language: hr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: hr\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -50,3 +51,10 @@ msgstr "Zadnje modificirano" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "Let's encrypt ne radi sa privatnim adresama ili lokalnim domenama." + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/hr_HR.po b/letsencrypt/i18n/hr_HR.po index 0d36127a36c..c79f6795ef6 100644 --- a/letsencrypt/i18n/hr_HR.po +++ b/letsencrypt/i18n/hr_HR.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: msgid "" msgstr "" @@ -10,12 +10,14 @@ msgstr "" "POT-Creation-Date: 2016-06-09 12:31+0000\n" "PO-Revision-Date: 2016-05-25 16:43+0000\n" "Last-Translator: <>\n" -"Language-Team: Croatian (Croatia) (http://www.transifex.com/oca/OCA-server-tools-9-0/language/hr_HR/)\n" +"Language-Team: Croatian (Croatia) (http://www.transifex.com/oca/OCA-server-" +"tools-9-0/language/hr_HR/)\n" +"Language: hr_HR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: hr_HR\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -28,7 +30,7 @@ msgid "Display Name" msgstr "Naziv" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:44 +#: code:addons/letsencrypt/models/letsencrypt.py:43 #, python-format msgid "Error calling %s: %d" msgstr "" @@ -44,7 +46,14 @@ msgid "Last Modified on" msgstr "Zadnje modificirano" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:93 +#: code:addons/letsencrypt/models/letsencrypt.py:90 #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/hu.po b/letsencrypt/i18n/hu.po index 6f402dd1d48..398660ae96f 100644 --- a/letsencrypt/i18n/hu.po +++ b/letsencrypt/i18n/hu.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Hungarian (https://www.transifex.com/oca/teams/23907/hu/)\n" +"Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: hu\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Utolsó frissítés dátuma" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/id.po b/letsencrypt/i18n/id.po index 6768761ff35..abb09e70ac3 100644 --- a/letsencrypt/i18n/id.po +++ b/letsencrypt/i18n/id.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Indonesian (https://www.transifex.com/oca/teams/23907/id/)\n" +"Language: id\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: id\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Terakhir Dimodifikasi pada" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/it.po b/letsencrypt/i18n/it.po index 98f284d3107..90a98849246 100644 --- a/letsencrypt/i18n/it.po +++ b/letsencrypt/i18n/it.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 # Paolo Valier , 2018 @@ -13,10 +13,10 @@ msgstr "" "PO-Revision-Date: 2018-01-06 02:25+0000\n" "Last-Translator: Paolo Valier , 2018\n" "Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\n" +"Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: it\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -50,3 +50,10 @@ msgstr "Ultima modifica il" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "Let's encrypt non funziona con indirizzi privati o domini locali!" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/ja.po b/letsencrypt/i18n/ja.po index d9594543422..be7085582ff 100644 --- a/letsencrypt/i18n/ja.po +++ b/letsencrypt/i18n/ja.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Japanese (https://www.transifex.com/oca/teams/23907/ja/)\n" +"Language: ja\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ja\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "最終更新日" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/ko.po b/letsencrypt/i18n/ko.po index 0074a455ba0..5dfc67f8958 100644 --- a/letsencrypt/i18n/ko.po +++ b/letsencrypt/i18n/ko.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Korean (https://www.transifex.com/oca/teams/23907/ko/)\n" +"Language: ko\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ko\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "최근 수정" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/letsencrypt.pot b/letsencrypt/i18n/letsencrypt.pot new file mode 100644 index 00000000000..a6db4f01186 --- /dev/null +++ b/letsencrypt/i18n/letsencrypt.pot @@ -0,0 +1,54 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * letsencrypt +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \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: letsencrypt +#: model:ir.model,name:letsencrypt.model_letsencrypt +msgid "Abstract model providing functions for letsencrypt" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name +msgid "Display Name" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:43 +#, python-format +msgid "Error calling %s: %d" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id +msgid "ID" +msgstr "" + +#. module: letsencrypt +#: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt___last_update +msgid "Last Modified on" +msgstr "" + +#. module: letsencrypt +#: code:addons/letsencrypt/models/letsencrypt.py:90 +#, python-format +msgid "Let's encrypt doesn't work with private addresses or local domains!" +msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" + diff --git a/letsencrypt/i18n/lt.po b/letsencrypt/i18n/lt.po index b0f042ab9ab..0bbc46872a3 100644 --- a/letsencrypt/i18n/lt.po +++ b/letsencrypt/i18n/lt.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Lithuanian (https://www.transifex.com/oca/teams/23907/lt/)\n" +"Language: lt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: lt\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n" +"%100<10 || n%100>=20) ? 1 : 2);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +50,10 @@ msgstr "Paskutinį kartą keista" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/lt_LT.po b/letsencrypt/i18n/lt_LT.po index e49e85f808d..382d3961b24 100644 --- a/letsencrypt/i18n/lt_LT.po +++ b/letsencrypt/i18n/lt_LT.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,12 +11,14 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Lithuanian (Lithuania) (https://www.transifex.com/oca/teams/23907/lt_LT/)\n" +"Language-Team: Lithuanian (Lithuania) (https://www.transifex.com/oca/" +"teams/23907/lt_LT/)\n" +"Language: lt_LT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: lt_LT\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n" +"%100<10 || n%100>=20) ? 1 : 2);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +51,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/lv.po b/letsencrypt/i18n/lv.po index 0fe3e0a0ece..9685e280bd7 100644 --- a/letsencrypt/i18n/lv.po +++ b/letsencrypt/i18n/lv.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Latvian (https://www.transifex.com/oca/teams/23907/lv/)\n" +"Language: lv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: lv\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : " +"2);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +50,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/mk.po b/letsencrypt/i18n/mk.po index 86eaec7003f..d021afc5366 100644 --- a/letsencrypt/i18n/mk.po +++ b/letsencrypt/i18n/mk.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Macedonian (https://www.transifex.com/oca/teams/23907/mk/)\n" +"Language: mk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: mk\n" "Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Последна промена на" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/mn.po b/letsencrypt/i18n/mn.po index 43016a84a16..a6818a961a3 100644 --- a/letsencrypt/i18n/mn.po +++ b/letsencrypt/i18n/mn.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Mongolian (https://www.transifex.com/oca/teams/23907/mn/)\n" +"Language: mn\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: mn\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Сүүлийн засвар хийсэн огноо" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/nb.po b/letsencrypt/i18n/nb.po index b602a047e99..671b9584413 100644 --- a/letsencrypt/i18n/nb.po +++ b/letsencrypt/i18n/nb.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Norwegian Bokmål (https://www.transifex.com/oca/teams/23907/nb/)\n" +"Language-Team: Norwegian Bokmål (https://www.transifex.com/oca/teams/23907/" +"nb/)\n" +"Language: nb\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: nb\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Sist oppdatert " #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/nb_NO.po b/letsencrypt/i18n/nb_NO.po index 2cfa2ff90ab..7531975d9f9 100644 --- a/letsencrypt/i18n/nb_NO.po +++ b/letsencrypt/i18n/nb_NO.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Norwegian Bokmål (Norway) (https://www.transifex.com/oca/teams/23907/nb_NO/)\n" +"Language-Team: Norwegian Bokmål (Norway) (https://www.transifex.com/oca/" +"teams/23907/nb_NO/)\n" +"Language: nb_NO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: nb_NO\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Sist endret den" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/nl.po b/letsencrypt/i18n/nl.po index 5aa027e8d10..c782bbfb460 100644 --- a/letsencrypt/i18n/nl.po +++ b/letsencrypt/i18n/nl.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Dutch (https://www.transifex.com/oca/teams/23907/nl/)\n" +"Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Laatst bijgewerkt op" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/nl_BE.po b/letsencrypt/i18n/nl_BE.po index 596879d3aec..61cbcde1d7b 100644 --- a/letsencrypt/i18n/nl_BE.po +++ b/letsencrypt/i18n/nl_BE.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Dutch (Belgium) (https://www.transifex.com/oca/teams/23907/nl_BE/)\n" +"Language-Team: Dutch (Belgium) (https://www.transifex.com/oca/teams/23907/" +"nl_BE/)\n" +"Language: nl_BE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: nl_BE\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Laatst Aangepast op" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/nl_NL.po b/letsencrypt/i18n/nl_NL.po index 7ecf33da748..db30227d5a7 100644 --- a/letsencrypt/i18n/nl_NL.po +++ b/letsencrypt/i18n/nl_NL.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # Peter Hageman , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: Peter Hageman , 2017\n" -"Language-Team: Dutch (Netherlands) (https://www.transifex.com/oca/teams/23907/nl_NL/)\n" +"Language-Team: Dutch (Netherlands) (https://www.transifex.com/oca/" +"teams/23907/nl_NL/)\n" +"Language: nl_NL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: nl_NL\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Laatst gewijzigd op" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/pl.po b/letsencrypt/i18n/pl.po index 735f2110d50..2483f809fdf 100644 --- a/letsencrypt/i18n/pl.po +++ b/letsencrypt/i18n/pl.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,13 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Polish (https://www.transifex.com/oca/teams/23907/pl/)\n" +"Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: pl\n" -"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" +"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" +"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +51,10 @@ msgstr "Ostatnio modyfikowano" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/pt.po b/letsencrypt/i18n/pt.po index 552c81552ce..ada7f7f5126 100644 --- a/letsencrypt/i18n/pt.po +++ b/letsencrypt/i18n/pt.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n" +"Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: pt\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Última Modificação Em" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/pt_BR.po b/letsencrypt/i18n/pt_BR.po index 7864acbc8be..5a87ef497fe 100644 --- a/letsencrypt/i18n/pt_BR.po +++ b/letsencrypt/i18n/pt_BR.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: msgid "" msgstr "" @@ -10,11 +10,12 @@ msgstr "" "POT-Creation-Date: 2016-07-09 10:34+0000\n" "PO-Revision-Date: 2016-05-25 16:43+0000\n" "Last-Translator: <>\n" -"Language-Team: Portuguese (Brazil) (http://www.transifex.com/oca/OCA-server-tools-9-0/language/pt_BR/)\n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/oca/OCA-server-" +"tools-9-0/language/pt_BR/)\n" +"Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: pt_BR\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: letsencrypt @@ -28,7 +29,7 @@ msgid "Display Name" msgstr "Nome para Mostrar" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:44 +#: code:addons/letsencrypt/models/letsencrypt.py:43 #, python-format msgid "Error calling %s: %d" msgstr "" @@ -44,7 +45,14 @@ msgid "Last Modified on" msgstr "Última atualização em" #. module: letsencrypt -#: code:addons/letsencrypt/models/letsencrypt.py:93 +#: code:addons/letsencrypt/models/letsencrypt.py:90 #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/pt_PT.po b/letsencrypt/i18n/pt_PT.po index 39fdd34d9a6..d9eb7372853 100644 --- a/letsencrypt/i18n/pt_PT.po +++ b/letsencrypt/i18n/pt_PT.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/teams/23907/pt_PT/)\n" +"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/" +"teams/23907/pt_PT/)\n" +"Language: pt_PT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: pt_PT\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "Última Modificação Em" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/ro.po b/letsencrypt/i18n/ro.po index fe6cd018192..89d0239e27b 100644 --- a/letsencrypt/i18n/ro.po +++ b/letsencrypt/i18n/ro.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Romanian (https://www.transifex.com/oca/teams/23907/ro/)\n" +"Language: ro\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ro\n" -"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" +"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?" +"2:1));\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +50,10 @@ msgstr "Ultima actualizare în" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/ru.po b/letsencrypt/i18n/ru.po index 7018c3dd293..f95efbabdf7 100644 --- a/letsencrypt/i18n/ru.po +++ b/letsencrypt/i18n/ru.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,13 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Russian (https://www.transifex.com/oca/teams/23907/ru/)\n" +"Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ru\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +51,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/sk.po b/letsencrypt/i18n/sk.po index f5f50459470..d6950b16fce 100644 --- a/letsencrypt/i18n/sk.po +++ b/letsencrypt/i18n/sk.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Slovak (https://www.transifex.com/oca/teams/23907/sk/)\n" +"Language: sk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: sk\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Posledná modifikácia" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/sl.po b/letsencrypt/i18n/sl.po index 179a156b0d4..74f5aec8b93 100644 --- a/letsencrypt/i18n/sl.po +++ b/letsencrypt/i18n/sl.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2017-12-01 02:10+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" +"Language: sl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: sl\n" -"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n" +"%100==4 ? 2 : 3);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +50,10 @@ msgstr "Zadnjič spremenjeno" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/sr.po b/letsencrypt/i18n/sr.po index e7294695d42..169813a4881 100644 --- a/letsencrypt/i18n/sr.po +++ b/letsencrypt/i18n/sr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Serbian (https://www.transifex.com/oca/teams/23907/sr/)\n" +"Language: sr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: sr\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +50,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/sr@latin.po b/letsencrypt/i18n/sr@latin.po index f750837d10a..f4bc5ddfccb 100644 --- a/letsencrypt/i18n/sr@latin.po +++ b/letsencrypt/i18n/sr@latin.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,12 +11,14 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Serbian (Latin) (https://www.transifex.com/oca/teams/23907/sr@latin/)\n" +"Language-Team: Serbian (Latin) (https://www.transifex.com/oca/teams/23907/" +"sr@latin/)\n" +"Language: sr@latin\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: sr@latin\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +51,10 @@ msgstr "Zadnja izmjena" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/sv.po b/letsencrypt/i18n/sv.po index 0e2caac4110..f6dbd871fc5 100644 --- a/letsencrypt/i18n/sv.po +++ b/letsencrypt/i18n/sv.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Swedish (https://www.transifex.com/oca/teams/23907/sv/)\n" +"Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: sv\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Senast redigerad" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/th.po b/letsencrypt/i18n/th.po index b63d3d64a2a..3d183650acb 100644 --- a/letsencrypt/i18n/th.po +++ b/letsencrypt/i18n/th.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Thai (https://www.transifex.com/oca/teams/23907/th/)\n" +"Language: th\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: th\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "แก้ไขครั้งสุดท้ายเมื่อ" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/tr.po b/letsencrypt/i18n/tr.po index 4f20a31f4aa..8fedacce809 100644 --- a/letsencrypt/i18n/tr.po +++ b/letsencrypt/i18n/tr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n" +"Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: tr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Son değişiklik" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/tr_TR.po b/letsencrypt/i18n/tr_TR.po index 5a1dbd80c9e..a719f9ef96d 100644 --- a/letsencrypt/i18n/tr_TR.po +++ b/letsencrypt/i18n/tr_TR.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Turkish (Turkey) (https://www.transifex.com/oca/teams/23907/tr_TR/)\n" +"Language-Team: Turkish (Turkey) (https://www.transifex.com/oca/teams/23907/" +"tr_TR/)\n" +"Language: tr_TR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: tr_TR\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "En son güncelleme tarihi" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/uk.po b/letsencrypt/i18n/uk.po index 48459c47a61..103f20fbb17 100644 --- a/letsencrypt/i18n/uk.po +++ b/letsencrypt/i18n/uk.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Ukrainian (https://www.transifex.com/oca/teams/23907/uk/)\n" +"Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: uk\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt @@ -49,3 +50,10 @@ msgstr "Остання модифікація" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/vi.po b/letsencrypt/i18n/vi.po index afea7f10e1e..31da4751836 100644 --- a/letsencrypt/i18n/vi.po +++ b/letsencrypt/i18n/vi.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" "Language-Team: Vietnamese (https://www.transifex.com/oca/teams/23907/vi/)\n" +"Language: vi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: vi\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +49,10 @@ msgstr "Sửa lần cuối vào" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/vi_VN.po b/letsencrypt/i18n/vi_VN.po index 0333fc1738b..6c570467916 100644 --- a/letsencrypt/i18n/vi_VN.po +++ b/letsencrypt/i18n/vi_VN.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Vietnamese (Viet Nam) (https://www.transifex.com/oca/teams/23907/vi_VN/)\n" +"Language-Team: Vietnamese (Viet Nam) (https://www.transifex.com/oca/" +"teams/23907/vi_VN/)\n" +"Language: vi_VN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: vi_VN\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/zh_CN.po b/letsencrypt/i18n/zh_CN.po index 8d5b0687992..f0325372df6 100644 --- a/letsencrypt/i18n/zh_CN.po +++ b/letsencrypt/i18n/zh_CN.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Chinese (China) (https://www.transifex.com/oca/teams/23907/zh_CN/)\n" +"Language-Team: Chinese (China) (https://www.transifex.com/oca/teams/23907/" +"zh_CN/)\n" +"Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "最后修改时间" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" diff --git a/letsencrypt/i18n/zh_TW.po b/letsencrypt/i18n/zh_TW.po index 220cd12c862..23ffef208de 100644 --- a/letsencrypt/i18n/zh_TW.po +++ b/letsencrypt/i18n/zh_TW.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * letsencrypt -# +# # Translators: # OCA Transbot , 2017 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2017-08-01 02:44+0000\n" "PO-Revision-Date: 2017-08-01 02:44+0000\n" "Last-Translator: OCA Transbot , 2017\n" -"Language-Team: Chinese (Taiwan) (https://www.transifex.com/oca/teams/23907/zh_TW/)\n" +"Language-Team: Chinese (Taiwan) (https://www.transifex.com/oca/teams/23907/" +"zh_TW/)\n" +"Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: zh_TW\n" "Plural-Forms: nplurals=1; plural=0;\n" #. module: letsencrypt @@ -49,3 +50,10 @@ msgstr "最後修改:" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" + +#. module: letsencrypt +#: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server +#: model:ir.cron,cron_name:letsencrypt.cronjob +#: model:ir.cron,name:letsencrypt.cronjob +msgid "Update letsencrypt certificates" +msgstr "" From 998e1f79ca29bec647be9c03dd7aafa6a767566e Mon Sep 17 00:00:00 2001 From: Rodrigo Macedo Date: Wed, 20 Jun 2018 13:14:52 +0000 Subject: [PATCH 10/57] =?UTF-8?q?Translated=20using=20Weblate=20(Portugu?= =?UTF-8?q?=C3=AAs=20(Brasil))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100,0% (7 of 7 strings) Translation: server-tools-11.0/server-tools-11.0-letsencrypt Translate-URL: https://translation.odoo-community.org/projects/server-tools-11-0/server-tools-11-0-letsencrypt/pt_BR/ --- letsencrypt/i18n/pt_BR.po | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/letsencrypt/i18n/pt_BR.po b/letsencrypt/i18n/pt_BR.po index 5a87ef497fe..32c9bc44c71 100644 --- a/letsencrypt/i18n/pt_BR.po +++ b/letsencrypt/i18n/pt_BR.po @@ -8,20 +8,21 @@ msgstr "" "Project-Id-Version: server-tools (9.0)\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-07-09 10:34+0000\n" -"PO-Revision-Date: 2016-05-25 16:43+0000\n" -"Last-Translator: <>\n" -"Language-Team: Portuguese (Brazil) (http://www.transifex.com/oca/OCA-server-" -"tools-9-0/language/pt_BR/)\n" +"PO-Revision-Date: 2018-06-20 13:15+0000\n" +"Last-Translator: Rodrigo Macedo \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/oca/" +"OCA-server-tools-9-0/language/pt_BR/)\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 3.0.1\n" #. module: letsencrypt #: model:ir.model,name:letsencrypt.model_letsencrypt msgid "Abstract model providing functions for letsencrypt" -msgstr "" +msgstr "Modelo abstrato fornecendo funções para letsencrypt" #. module: letsencrypt #: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_display_name @@ -32,7 +33,7 @@ msgstr "Nome para Mostrar" #: code:addons/letsencrypt/models/letsencrypt.py:43 #, python-format msgid "Error calling %s: %d" -msgstr "" +msgstr "Erro ao ligar %s: %d" #. module: letsencrypt #: model:ir.model.fields,field_description:letsencrypt.field_letsencrypt_id @@ -49,10 +50,11 @@ msgstr "Última atualização em" #, python-format msgid "Let's encrypt doesn't work with private addresses or local domains!" msgstr "" +"Vamos criptografar não funciona com endereços privados ou domínios locais!" #. module: letsencrypt #: model:ir.actions.server,name:letsencrypt.cronjob_ir_actions_server #: model:ir.cron,cron_name:letsencrypt.cronjob #: model:ir.cron,name:letsencrypt.cronjob msgid "Update letsencrypt certificates" -msgstr "" +msgstr "Atualizar os certificados do letsencrypt" From 356ae48fae0707d527a877bbcc160665c50b6015 Mon Sep 17 00:00:00 2001 From: George Daramouskas Date: Thu, 8 Feb 2018 17:23:09 +0100 Subject: [PATCH 11/57] [ADD] Adds support for Acme V2 --- letsencrypt/README.rst | 59 +-- letsencrypt/__manifest__.py | 21 +- letsencrypt/controllers/main.py | 4 +- letsencrypt/data/ir_config_parameter.xml | 22 +- letsencrypt/data/ir_cron.xml | 2 +- letsencrypt/hooks.py | 2 +- .../migrations/11.0.2.0.0/post-migrate.py | 27 ++ letsencrypt/models/__init__.py | 1 + letsencrypt/models/base_config_settings.py | 75 ++++ letsencrypt/models/letsencrypt.py | 340 +++++++++++------- letsencrypt/tests/__init__.py | 2 +- letsencrypt/tests/test_letsencrypt.py | 109 +++++- letsencrypt/views/base_config_settings.xml | 33 ++ 13 files changed, 517 insertions(+), 180 deletions(-) create mode 100644 letsencrypt/migrations/11.0.2.0.0/post-migrate.py create mode 100644 letsencrypt/models/base_config_settings.py create mode 100644 letsencrypt/views/base_config_settings.xml diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst index b1b419bcd28..46735c0989e 100644 --- a/letsencrypt/README.rst +++ b/letsencrypt/README.rst @@ -23,22 +23,6 @@ the SSL version. After installation, trigger the cronjob `Update letsencrypt certificates` and watch your log for messages. -This addon depends on the ``openssl`` binary and the ``acme_tiny`` and ``IPy`` -python modules. - -For installing the OpenSSL binary you can use your distro package manager. -For Debian and Ubuntu, that would be: - - sudo apt-get install openssl - -For installing the ACME-Tiny python module, use the PIP package manager: - - sudo pip install acme-tiny - -For installing the IPy python module, use the PIP package manager: - - sudo pip install IPy - Configuration ============= @@ -47,15 +31,26 @@ This addons requests a certificate for the domain named in the configuration parameter ``web.base.url`` - if this comes back as ``localhost`` or the like, the module doesn't request anything. -If you want your certificate to contain multiple alternative names, just add -them as configuration parameters ``letsencrypt.altname.N`` with ``N`` starting -from ``0``. The amount of domains that can be added are subject to `rate +Futher self-explanatory settings are in Settings -> General Settings. There you +can add further domains to the CSR, add a custom script that updates your DNS +and add a script that will be used to reload your web server (if needed). +The amount of domains that can be added are subject to `rate limiting `_. Note that all those domains must be publicly reachable on port 80 via HTTP, and they must have an entry for ``.well-known/acme-challenge`` pointing to your odoo instance. +Since DNS changes can take some time to propagate, when we respond to a DNS challenge +and the server tries to check our response, it might fail (and probably will). +The solution to this is documented in https://tools.ietf.org/html/rfc8555#section-8.2 +and basically is a `Retry-After` header under which we can instruct the server to +retry the challenge. +At the time these lines were written, Boulder had not implemented this functionality. +This prompted us to use `letsencrypt_backoff` configuration parameter, which is the +amount of minutes this module will try poll the server to retry validating the answer +to our challenge, specifically it is the `deadline` parameter of `poll_and_finalize`. + Usage ===== @@ -75,15 +70,22 @@ For further information, please visit: In depth configuration ====================== -This module uses ``openssl`` to generate CSRs suitable to be submitted to -letsencrypt.org. In order to do this, it copies ``/etc/ssl/openssl.cnf`` to a -temporary and adapts it according to its needs (currently, that's just adding a -``[SAN]`` section if necessary). If you want the module to use another configuration -template, set config parameter ``letsencrypt.openssl.cnf``. +If you want to use multiple domains on your CSR then you have to configure them +from Settings -> General Settings. If you use a wildcard in any of those domains +then letsencrypt will return a DNS challenge. In order for that challenge to be +answered you will need to **either** provide a script (as seen in General Settings) +or install a module that provides support for your VPS. In that module you will +need to create a function in the letsencrypt model with the name +`_respond_challenge_dns_$DNS_PROVIDER` where `$DNS_PROVIDER` is the name of your +provider and can be any string with length greater than zero, and add the name +of your DNS provider in the settings dns_provider selection field. + +In any case if a script path is inserted in the settings page, it will be run +in case you want to update multiple DNS servers. + +A reload command can be set in the Settings as well in case you need to reload +your web server. This by default is ``sudo /usr/sbin/service nginx reload`` -After refreshing the certificate, the module attempts to run the content of -``letsencrypt.reload_command``, which is by default ``sudo service nginx reload``. -Change this to match your server's configuration. You'll also need a matching sudo configuration, like:: @@ -141,11 +143,12 @@ Contributors * Dave Lasley * Ronald Portier * Ignacio Ibeas +* George Daramouskas ACME implementation ------------------- -* https://github.com/diafygi/acme-tiny/blob/master/acme_tiny.py +* https://github.com/certbot/certbot/tree/0.22.x/acme Icon ---- diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index 5c2984289b2..ed1c796ed57 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { "name": "Let's encrypt", - "version": "11.0.1.0.0", + "version": "11.0.2.0.0", "author": "Therp BV," "Tecnativa," "Acysos S.L," @@ -11,22 +11,23 @@ "category": "Hidden/Dependency", "summary": "Request SSL certificates from letsencrypt.org", "depends": [ - 'base', + "base_setup", ], "data": [ "data/ir_config_parameter.xml", "data/ir_cron.xml", "demo/ir_cron.xml", + "views/base_config_settings.xml", ], - "post_init_hook": 'post_init_hook', - 'installable': True, + "post_init_hook": "post_init_hook", + "installable": True, "external_dependencies": { - 'bin': [ - 'openssl', - ], - 'python': [ - 'acme_tiny', - 'IPy', + "python": [ + "acme", + "cryptography", + "josepy", + "IPy", + "OpenSSL", ], }, } diff --git a/letsencrypt/controllers/main.py b/letsencrypt/controllers/main.py index 9d9599117e3..bba661710ce 100644 --- a/letsencrypt/controllers/main.py +++ b/letsencrypt/controllers/main.py @@ -5,14 +5,14 @@ import os from odoo import http from odoo.http import request -from ..models.letsencrypt import get_challenge_dir +from ..models.letsencrypt import _get_challenge_dir class Letsencrypt(http.Controller): @http.route('/.well-known/acme-challenge/', auth='none') def acme_challenge(self, filename): try: - with open(os.path.join(get_challenge_dir(), filename)) as key: + with open(os.path.join(_get_challenge_dir(), filename)) as key: return key.read() except IOError: pass diff --git a/letsencrypt/data/ir_config_parameter.xml b/letsencrypt/data/ir_config_parameter.xml index 13f91f2edbc..e7932e7af14 100644 --- a/letsencrypt/data/ir_config_parameter.xml +++ b/letsencrypt/data/ir_config_parameter.xml @@ -1,9 +1,25 @@ - + - + + letsencrypt.reload_command sudo /usr/sbin/service nginx reload + + + letsencrypt_backoff + 3 + + + - + diff --git a/letsencrypt/data/ir_cron.xml b/letsencrypt/data/ir_cron.xml index f2f065cd9e3..aab48cc528b 100644 --- a/letsencrypt/data/ir_cron.xml +++ b/letsencrypt/data/ir_cron.xml @@ -5,7 +5,7 @@ Update letsencrypt certificates code - model.cron() + model._cron() 11 weeks -1 diff --git a/letsencrypt/hooks.py b/letsencrypt/hooks.py index 88ac2e8892e..51477ae97f9 100644 --- a/letsencrypt/hooks.py +++ b/letsencrypt/hooks.py @@ -5,4 +5,4 @@ def post_init_hook(cr, pool): env = api.Environment(cr, SUPERUSER_ID, {}) - env['letsencrypt'].generate_account_key() + env['letsencrypt']._generate_key('account_key') diff --git a/letsencrypt/migrations/11.0.2.0.0/post-migrate.py b/letsencrypt/migrations/11.0.2.0.0/post-migrate.py new file mode 100644 index 00000000000..0340c51af26 --- /dev/null +++ b/letsencrypt/migrations/11.0.2.0.0/post-migrate.py @@ -0,0 +1,27 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, SUPERUSER_ID + + +def migrate_altnames(env): + ir_config_parameter = env['ir.config_parameter'] + new_domains = ','.join(ir_config_parameter.search([ + ('key', '=like', 'letsencrypt.altname.%')]).mapped('value')) + ir_config_parameter.set_param('letsencrypt_altnames', new_domains) + ir_config_parameter.search([ + ('key', '=like', 'letsencrypt.altname.%')]).unlink() + + +def migrate_cron(env): + ir_cron = env['ir.cron'] + old_cron = ir_cron.search([ + ('model', '=', 'letsencrypt'), + ('function', '=', 'cron')]) + if old_cron: + old_cron.write({'function': '_cron'}) + + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + migrate_altnames(env) + migrate_cron(env) diff --git a/letsencrypt/models/__init__.py b/letsencrypt/models/__init__.py index 953f1c5d0aa..bb6955a8e93 100644 --- a/letsencrypt/models/__init__.py +++ b/letsencrypt/models/__init__.py @@ -1,3 +1,4 @@ # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import letsencrypt +from . import base_config_settings diff --git a/letsencrypt/models/base_config_settings.py b/letsencrypt/models/base_config_settings.py new file mode 100644 index 00000000000..4634e2a2fdd --- /dev/null +++ b/letsencrypt/models/base_config_settings.py @@ -0,0 +1,75 @@ +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + + +class BaseConfigSettings(models.TransientModel): + _inherit = 'base.config.settings' + + dns_provider = fields.Selection( + [('shell', 'Shell')], + string='DNS Provider', + help='If we need to respond to a DNS challenge we need to add ' + 'a TXT record on your DNS. If you leave this to Shell ' + 'then you signify to the module that this will be taken ' + 'care off by that script written below. ' + 'Generally new modules that are made ' + 'to support various VPS providers add attributes here.', + ) + script = fields.Text( + 'Script', + help='Write your script that will update your DNS TXT records.', + ) + altnames = fields.Text( + default='', + help='Domains for which you want to include on the CSR ' + 'Separate with commas.', + ) + needs_dns_provider = fields.Boolean() + reload_command = fields.Text( + 'Reload Command', + help='Fill this with the command to restart your web server.', + ) + + @api.onchange('altnames') + def onchange_altnames(self): + if self.altnames: + self.needs_dns_provider = any( + '*.' in altname for altname in self.altnames.split(',')) + + @api.model + def default_get(self, field_list): + res = super(BaseConfigSettings, self).default_get(field_list) + ir_config_parameter = self.env['ir.config_parameter'] + res.update({ + 'dns_provider': ir_config_parameter.get_param( + 'letsencrypt_dns_provider'), + 'script': ir_config_parameter.get_param( + 'letsencrypt_script'), + 'altnames': ir_config_parameter.get_param( + 'letsencrypt_altnames'), + 'reload_command': ir_config_parameter.get_param( + 'letsencrypt.reload_command'), + }) + return res + + @api.multi + def set_dns_provider(self): + self.ensure_one() + ir_config_parameter = self.env['ir.config_parameter'] + ir_config_parameter.set_param( + 'letsencrypt_dns_provider', + self.dns_provider) + ir_config_parameter.set_param( + 'letsencrypt_needs_dns_provider', + self.needs_dns_provider) + ir_config_parameter.set_param( + 'letsencrypt_script', + self.script) + ir_config_parameter.set_param( + 'letsencrypt_altnames', + self.altnames) + ir_config_parameter.set_param( + 'letsencrypt.reload_command', + self.reload_command) + return True diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 8899f135a8c..6f61f0969b3 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -2,26 +2,50 @@ # © 2016 Antonio Espinosa # © 2018 Ignacio Ibeas # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import os +from datetime import datetime, timedelta +from os.path import join, isdir, isfile +from os import makedirs +from odoo import _, api, models, exceptions +from odoo.tools import config import logging -import urllib.request import urllib.parse import subprocess -import tempfile -from odoo import _, api, models, exceptions -from odoo.tools import config +import requests +import base64 +import os +import re + +_logger = logging.getLogger(__name__) + +try: + from cryptography.hazmat.backends import default_backend + import josepy as jose + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives import serialization, hashes + from acme import client, crypto_util, errors + from acme.messages import Registration, NewRegistration, \ + RegistrationResource + from acme import challenges + import IPy +except ImportError as e: + _logger.debug(e) +WILDCARD = '*.' # as defined in the spec DEFAULT_KEY_LENGTH = 4096 -_logger = logging.getLogger(__name__) +TYPE_CHALLENGE_HTTP = 'http-01' +TYPE_CHALLENGE_DNS = 'dns-01' +V2_STAGING_DIRECTORY_URL = \ + 'https://acme-staging-v02.api.letsencrypt.org/directory' +V2_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory' -def get_data_dir(): - return os.path.join(config.options.get('data_dir'), 'letsencrypt') +def _get_data_dir(): + return join(config.options.get('data_dir'), 'letsencrypt') -def get_challenge_dir(): - return os.path.join(get_data_dir(), 'acme-challenge') +def _get_challenge_dir(): + return join(_get_data_dir(), 'acme-challenge') class Letsencrypt(models.AbstractModel): @@ -29,140 +53,208 @@ class Letsencrypt(models.AbstractModel): _description = 'Abstract model providing functions for letsencrypt' @api.model - def call_cmdline(self, cmdline, loglevel=logging.INFO, - raise_on_result=True): - process = subprocess.Popen( - cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - if stderr: - _logger.log(loglevel, stderr) - if stdout: - _logger.log(loglevel, stdout) - if process.returncode: - raise exceptions.Warning( - _('Error calling %s: %d') % (cmdline[0], process.returncode) - ) - return process.returncode - - @api.model - def generate_account_key(self): - data_dir = get_data_dir() - if not os.path.isdir(data_dir): - os.makedirs(data_dir) - account_key = os.path.join(data_dir, 'account.key') - if not os.path.isfile(account_key): - _logger.info('generating rsa account key') - self.call_cmdline([ - 'openssl', 'genrsa', '-out', account_key, - str(DEFAULT_KEY_LENGTH), - ]) - assert os.path.isfile(account_key), 'failed to create rsa key' - return account_key - - @api.model - def generate_domain_key(self, domain): - domain_key = os.path.join(get_data_dir(), '%s.key' % domain) - if not os.path.isfile(domain_key): - _logger.info('generating rsa domain key for %s', domain) - self.call_cmdline([ - 'openssl', 'genrsa', '-out', domain_key, - str(DEFAULT_KEY_LENGTH), - ]) - return domain_key + def _generate_key(self, key_name): + _logger.info('Generating key ' + str(key_name)) + data_dir = _get_data_dir() + if not isdir(data_dir): + makedirs(data_dir) + key_file = join(data_dir, key_name) + if not isfile(key_file): + _logger.info('Generating a new key') + key_json = jose.JWKRSA(key=jose.ComparableRSAKey( + rsa.generate_private_key( + public_exponent=65537, + key_size=DEFAULT_KEY_LENGTH, + backend=default_backend()))) + key = key_json.key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption()) + with open(key_file, 'wb') as _file: + _file.write(key) + return key_file @api.model - def validate_domain(self, domain): + def _validate_domain(self, domain): local_domains = [ 'localhost', 'localhost.localdomain', 'localhost6', 'localhost6.localdomain6' ] def _ip_is_private(address): - import IPy try: ip = IPy.IP(address) - except Exception: + except ValueError: return False return ip.iptype() == 'PRIVATE' if domain in local_domains or _ip_is_private(domain): raise exceptions.Warning( _("Let's encrypt doesn't work with private addresses " - "or local domains!")) - - @api.model - def generate_csr(self, domain): - domains = [domain] - parameter_model = self.env['ir.config_parameter'] - altnames = parameter_model.search( - [('key', 'like', 'letsencrypt.altname.')], - order='key' - ) - for altname in altnames: - domains.append(altname.value) - _logger.info('generating csr for %s', domain) - if len(domains) > 1: - _logger.info('with alternative subjects %s', ','.join(domains[1:])) - config = parameter_model.get_param( - 'letsencrypt.openssl.cnf', '/etc/ssl/openssl.cnf' - ) - csr = os.path.join(get_data_dir(), '%s.csr' % domain) - with tempfile.NamedTemporaryFile(mode='wt') as cfg: - cfg.write(open(config).read()) - if len(domains) > 1: - cfg.write( - '\n[SAN]\nsubjectAltName=' + - ','.join(['DNS:%s' % x for x in domains]) + '\n') - cfg.file.flush() - cmdline = [ - 'openssl', 'req', '-new', - parameter_model.get_param( - 'letsencrypt.openssl.digest', '-sha256'), - '-key', self.generate_domain_key(domain), - '-subj', '/CN=%s' % domain, '-config', cfg.name, - '-out', csr, - ] - if len(domains) > 1: - cmdline.extend([ - '-reqexts', 'SAN', - ]) - self.call_cmdline(cmdline) - return csr + "or local domains!")) @api.model - def cron(self): + def _cron(self): + ir_config_parameter = self.env['ir.config_parameter'] domain = urllib.parse.urlparse( self.env['ir.config_parameter'].get_param( 'web.base.url', 'localhost')).netloc - self.validate_domain(domain) - account_key = self.generate_account_key() - csr = self.generate_csr(domain) - acme_challenge = get_challenge_dir() - if not os.path.isdir(acme_challenge): - os.makedirs(acme_challenge) - if self.env.context.get('letsencrypt_dry_run'): - crt_text = 'I\'m a test text' - else: # pragma: no cover - from acme_tiny import get_crt, DEFAULT_CA - crt_text = get_crt( - account_key, csr, acme_challenge, log=_logger, CA=DEFAULT_CA) - with open(os.path.join(get_data_dir(), '%s.crt' % domain), 'w')\ - as crt: - crt.write(crt_text) - chain_cert = urllib.request.urlopen( - self.env['ir.config_parameter'].get_param( - 'letsencrypt.chain_certificate_address', - 'https://letsencrypt.org/certs/' - 'lets-encrypt-x3-cross-signed.pem') + self._validate_domain(domain) + # Generate account key + account_key_file = self._generate_key('account.key') + account_key = jose.JWKRSA.load(open(account_key_file).read()) + # Generate domain key + domain_key_file = self._generate_key(domain) + client = self._create_client(account_key) + new_reg = NewRegistration( + key=account_key.public_key(), + terms_of_service_agreed=True) + try: + client.new_account(new_reg) + except errors.ConflictError as e: + reg = Registration(key=account_key.public_key()) + reg_res = RegistrationResource( + body=reg, + uri=e.location, ) - crt.write(str(chain_cert.read())) - chain_cert.close() - _logger.info('wrote %s', crt.name) - reload_cmd = self.env['ir.config_parameter'].sudo().get_param( - 'letsencrypt.reload_command', False) - if reload_cmd: - _logger.info('reloading webserver...') - self.call_cmdline(['sh', '-c', reload_cmd]) + client.query_registration(reg_res) + csr = self._make_csr(account_key, domain_key_file, domain) + authzr = client.new_order(csr) + auth_responded = False + for authorizations in authzr.authorizations: + for challenge in sorted( + authorizations.body.challenges, + key=lambda x: x.chall.typ == TYPE_CHALLENGE_HTTP, + reverse=True): + if challenge.chall.typ == TYPE_CHALLENGE_HTTP: + self._respond_challenge_http(challenge, account_key) + client.answer_challenge( + challenge, + challenges.HTTP01Response()) + auth_responded = True + break + elif challenge.chall.typ == TYPE_CHALLENGE_DNS: + self._respond_challenge_dns( + challenge, + account_key, + authorizations.body.identifier.value, + ) + client.answer_challenge( + challenge, challenges.DNSResponse()) + auth_responded = True + break + if not auth_responded: + raise exceptions.ValidationError( + _('Could not respond to letsencrypt challenges.')) + # let them know we are done and they should check + deadline = datetime.now() + timedelta( + minutes=int( + ir_config_parameter.get_param('letsencrypt_backoff', 3))) + order_resource = client.poll_and_finalize(authzr, deadline) + with open(join(_get_data_dir(), '%s.crt' % domain), 'w') as crt: + crt.write(order_resource.fullchain_pem) + _logger.info('SUCCESS: Certificate saved :%s', crt.name) + reload_cmd = ir_config_parameter.get_param( + 'letsencrypt.reload_command', False) + if reload_cmd: + self._call_cmdline(['sh', '-c', reload_cmd]) + else: + _logger.warning("No reload command defined.") + + def _create_client(self, account_key): + net = client.ClientNetwork(account_key) + if config['test_enable']: + directory_url = V2_STAGING_DIRECTORY_URL else: - _logger.info('no command defined for reloading webserver, please ' - 'do it manually in order to apply new certificate') + directory_url = V2_DIRECTORY_URL + directory_json = requests.get(directory_url).json() + return client.ClientV2(directory_json, net) + + def _cascade_domains(self, altnames): + """ Given an list of domains containing one or more wildcard domains + the following are performed: + 1) for every wildcard domain: + a) gets the index of it's wildcard characters + b) if there are non wildcard domain names that are the same + after the index of the current wildcard name remove them. + 2) when done, return the modified altnames + """ + for altname in filter(lambda x: WILDCARD in x, altnames): + pat = re.compile('^.*' + altname.replace(WILDCARD, '') + '.*$') + for _altname in filter(lambda x: WILDCARD not in x, altnames): + if pat.search(_altname): + altnames.remove(_altname) + return altnames + + def _make_csr(self, account_key, domain_key_file, domain): + parameter_model = self.env['ir.config_parameter'] + altnames = parameter_model.get_param('letsencrypt_altnames') + if altnames: + altnames = re.split(',|\n| |;', altnames) + valid_domains = altnames + [domain] + valid_domains = self._cascade_domains(valid_domains) + else: + valid_domains = [domain] + _logger.info( + 'Making CSR for the following domains: ' + str(valid_domains)) + return crypto_util.make_csr( + open(domain_key_file).read(), valid_domains) + + def _respond_challenge_http(self, challenge, account_key): + """ + Respond to the HTTP challenge by writing the file to serve. + """ + challenge_dir = _get_challenge_dir() + if not isdir(challenge_dir): + makedirs(challenge_dir) + token = base64.urlsafe_b64encode(challenge.token) + challenge_file = join(_get_challenge_dir(), '%s' % token.rstrip('=')) + with open(challenge_file, 'wb') as challenge_file: + challenge_file.write(token.rstrip('=') + '.' + jose.b64encode( + account_key.thumbprint(hash_function=hashes.SHA256)).decode()) + + def _respond_challenge_dns(self, challenge, account_key, domain): + """ + Respond to the DNS challenge by creating the DNS record + on the provider. + """ + letsencrypt_dns_function = '_respond_challenge_dns_' + \ + self.env['ir.config_parameter'].get_param( + 'letsencrypt_dns_provider') + getattr(self, letsencrypt_dns_function)(challenge, account_key, domain) + + @api.model + def _call_cmdline(self, cmdline, env=None, shell=False): + process = subprocess.Popen( + cmdline, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + shell=shell, + ) + stdout, stderr = process.communicate() + if process.returncode: + raise exceptions.Warning(_( + 'Error calling %s: %s %s %s' % ( + cmdline, + str(process.returncode) + ' '.join(cmdline), + stdout, + stderr, + ))) + + @api.model + def _respond_challenge_dns_shell(self, challenge, account_key, domain): + script_str = self.env['ir.config_parameter'].get_param( + 'letsencrypt_script') + if script_str: + env = os.environ + env.update( + LETSENCRYPT_DNS_CHALLENGE=jose.encode_b64jose( + challenge.chall.token), + LETSENCRYPT_DNS_DOMAIN=domain, + ) + self.env['letsencrypt']._call_cmdline( + script_str, + env=env, + shell=True, + ) diff --git a/letsencrypt/tests/__init__.py b/letsencrypt/tests/__init__.py index c5eb768cad8..4a7abea1fc7 100644 --- a/letsencrypt/tests/__init__.py +++ b/letsencrypt/tests/__init__.py @@ -1,3 +1,3 @@ -# © 2016 Therp BV +# Copyright 2018 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import test_letsencrypt diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index 60616d9af7e..a9fce6f410a 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -1,13 +1,102 @@ -# © 2016 Therp BV +# Copyright 2018 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo.tests.common import TransactionCase +from odoo.tests import SingleTransactionCase +from ..models.letsencrypt import _get_data_dir, WILDCARD +from os import path +import shutil +import mock +import os -class TestLetsencrypt(TransactionCase): - def test_letsencrypt(self): - from ..hooks import post_init_hook - post_init_hook(self.cr, None) - self.env.ref('letsencrypt.config_parameter_reload').write({ - 'value': '', - }) - self.env['letsencrypt'].with_context(letsencrypt_dry_run=True).cron() +class TestLetsencrypt(SingleTransactionCase): + + post_install = True + at_install = False + + def setUp(self): + super(TestLetsencrypt, self).setUp() + + def test_config_settings(self): + settings_model = self.env['base.config.settings'] + letsencrypt_model = self.env['letsencrypt'] + settings = settings_model.create({ + 'dns_provider': 'shell', + 'script': 'touch /tmp/.letsencrypt_test', + 'altnames': + 'test.example.com', + 'reload_command': 'echo', + }) + self.env['ir.config_parameter'].set_param( + 'web.base.url', 'http://www.example.com') + settings.set_dns_provider() + setting_vals = settings.default_get([]) + self.assertEquals(setting_vals['dns_provider'], 'shell') + letsencrypt_model._call_cmdline(setting_vals['script'], shell=True) + self.assertEquals(path.exists('/tmp/.letsencrypt_test'), True) + self.assertEquals( + setting_vals['altnames'], + settings.altnames, + ) + self.assertEquals(setting_vals['reload_command'], 'echo') + settings.onchange_altnames() + self.assertEquals(settings.needs_dns_provider, False) + settings.unlink() + + def new_order(self, typ): + if typ not in ['http-01', 'dns-01']: + raise ValueError + authzr = mock.Mock + order_resource = mock.Mock + order_resource.fullchain_pem = 'test' + authorization = mock.Mock + body = mock.Mock + challenge = mock.Mock + challenge.chall = mock.Mock + challenge.chall.typ = typ + challenge.chall.token = 'a_token' + body.challenges = [challenge] + authorization.body = body + authzr.authorizations = [authorization] + return authzr + + def poll(self, deadline): + order_resource = mock.Mock + order_resource.fullchain_pem = 'chain' + return order_resource + + @mock.patch('acme.client.ClientV2.answer_challenge') + @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=poll) + def test_http_challenge(self, poll, answer_challenge): + letsencrypt = self.env['letsencrypt'] + self.env['ir.config_parameter'].set_param( + 'web.base.url', 'http://www.example.com') + settings = self.env['base.config.settings'] + settings.create({ + 'altnames': 'test.example.com', + }).set_dns_provider() + letsencrypt._cron() + poll.assert_called() + self.assertTrue( + open(path.join(_get_data_dir(), 'www.example.com.crt')).read(), + ) + os.remove(path.join(_get_data_dir(), 'www.example.com.crt')) + + @mock.patch('acme.client.ClientV2.answer_challenge') + @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=poll) + def test_dns_challenge(self, poll, answer_challenge): + letsencrypt = self.env['letsencrypt'] + settings = self.env['base.config.settings'] + settings.create({ + 'dns_provider': 'shell', + 'script': 'echo', + 'altnames': WILDCARD + 'example.com', + }).set_dns_provider() + letsencrypt._cron() + poll.assert_called() + self.assertTrue( + open(path.join(_get_data_dir(), 'www.example.com.crt')).read(), + ) + + def tearDown(self): + super(TestLetsencrypt, self).tearDown() + shutil.rmtree(_get_data_dir(), ignore_errors=True) diff --git a/letsencrypt/views/base_config_settings.xml b/letsencrypt/views/base_config_settings.xml new file mode 100644 index 00000000000..6637a7cafce --- /dev/null +++ b/letsencrypt/views/base_config_settings.xml @@ -0,0 +1,33 @@ + + + + Letsencrypt base config settings + base.config.settings + + + + + + + + + + + + + + + + + From 55388439e2567c9845622d9af94d3d58c20ceda0 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Fri, 15 Nov 2019 17:19:36 +0100 Subject: [PATCH 12/57] [IMP] Refactor and debug Let's Encrypt ACMEv2 support --- letsencrypt/README.rst | 106 ++-- letsencrypt/__manifest__.py | 9 +- letsencrypt/data/ir_config_parameter.xml | 2 +- letsencrypt/data/ir_cron.xml | 6 +- letsencrypt/demo/ir_cron.xml | 12 +- letsencrypt/hooks.py | 2 +- .../migrations/11.0.2.0.0/post-migrate.py | 31 +- letsencrypt/models/base_config_settings.py | 164 ++++-- letsencrypt/models/letsencrypt.py | 550 ++++++++++++------ letsencrypt/readme/CONFIGURE.rst | 24 + letsencrypt/readme/CONTRIBUTORS.rst | 6 + letsencrypt/readme/CREDITS.rst | 9 + letsencrypt/readme/DESCRIPTION.rst | 2 + letsencrypt/readme/INSTALL.rst | 10 + letsencrypt/readme/USAGE.rst | 63 ++ letsencrypt/static/description/index.html | 544 +++++++++++++++++ letsencrypt/tests/test_letsencrypt.py | 377 +++++++++--- letsencrypt/views/base_config_settings.xml | 24 +- 18 files changed, 1571 insertions(+), 370 deletions(-) create mode 100644 letsencrypt/readme/CONFIGURE.rst create mode 100644 letsencrypt/readme/CONTRIBUTORS.rst create mode 100644 letsencrypt/readme/CREDITS.rst create mode 100644 letsencrypt/readme/DESCRIPTION.rst create mode 100644 letsencrypt/readme/INSTALL.rst create mode 100644 letsencrypt/readme/USAGE.rst create mode 100644 letsencrypt/static/description/index.html diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst index 46735c0989e..dba2941dfb4 100644 --- a/letsencrypt/README.rst +++ b/letsencrypt/README.rst @@ -1,14 +1,38 @@ -.. 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 +============= +Let's Encrypt +============= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/10.0/letsencrypt + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-10-0/server-tools-10-0-letsencrypt + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/149/10.0 + :alt: Try me on Runbot -============================================= -Request SSL certificates from letsencrypt.org -============================================= +|badge1| |badge2| |badge3| |badge4| |badge5| This module was written to have your Odoo installation request SSL certificates from https://letsencrypt.org automatically. +**Table of contents** + +.. contents:: + :local: + Installation ============ @@ -23,7 +47,6 @@ the SSL version. After installation, trigger the cronjob `Update letsencrypt certificates` and watch your log for messages. - Configuration ============= @@ -34,49 +57,45 @@ the module doesn't request anything. Futher self-explanatory settings are in Settings -> General Settings. There you can add further domains to the CSR, add a custom script that updates your DNS and add a script that will be used to reload your web server (if needed). -The amount of domains that can be added are subject to `rate -limiting `_. +The number of domains that can be added to a certificate is +`capped at 100 `_. A wildcard +certificate can be used to avoid that limit. Note that all those domains must be publicly reachable on port 80 via HTTP, and -they must have an entry for ``.well-known/acme-challenge`` pointing to your odoo -instance. +they must have an entry for ``.well-known/acme-challenge`` pointing to +``$datadir/letsencrypt/acme-challenge`` of your odoo instance. Since DNS changes can take some time to propagate, when we respond to a DNS challenge and the server tries to check our response, it might fail (and probably will). The solution to this is documented in https://tools.ietf.org/html/rfc8555#section-8.2 -and basically is a `Retry-After` header under which we can instruct the server to +and basically is a ``Retry-After`` header under which we can instruct the server to retry the challenge. At the time these lines were written, Boulder had not implemented this functionality. -This prompted us to use `letsencrypt_backoff` configuration parameter, which is the +This prompted us to use ``letsencrypt.backoff`` configuration parameter, which is the amount of minutes this module will try poll the server to retry validating the answer -to our challenge, specifically it is the `deadline` parameter of `poll_and_finalize`. +to our challenge, specifically it is the ``deadline`` parameter of ``poll_and_finalize``. Usage ===== The module sets up a cronjob that requests and renews certificates automatically. +Certificates are renewed a month before they expire. Renewal is then attempted +every day until it succeeds. + After the first run, you'll find a file called ``domain.crt`` in ``$datadir/letsencrypt``, configure your SSL proxy to use this file as certificate. -.. 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/8.0 - -For further information, please visit: - -* https://www.odoo.com/forum/help-1 - In depth configuration -====================== +~~~~~~~~~~~~~~~~~~~~~~ If you want to use multiple domains on your CSR then you have to configure them from Settings -> General Settings. If you use a wildcard in any of those domains then letsencrypt will return a DNS challenge. In order for that challenge to be answered you will need to **either** provide a script (as seen in General Settings) -or install a module that provides support for your VPS. In that module you will -need to create a function in the letsencrypt model with the name -`_respond_challenge_dns_$DNS_PROVIDER` where `$DNS_PROVIDER` is the name of your +or install a module that provides support for your DNS provider. In that module +you will need to create a function in the letsencrypt model with the name +``_respond_challenge_dns_$DNS_PROVIDER`` where ``$DNS_PROVIDER`` is the name of your provider and can be any string with length greater than zero, and add the name of your DNS provider in the settings dns_provider selection field. @@ -123,20 +142,27 @@ you need to add ``letsencrypt`` addon to wide load addons list (by default, only ``web`` addon), setting ``--load`` option. For example, ``--load=web,letsencrypt`` - 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 -`here `_. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. Credits ======= +Authors +~~~~~~~ + +* Therp BV +* Tecnativa + Contributors ------------- +~~~~~~~~~~~~ * Holger Brunn * Antonio Espinosa @@ -144,28 +170,34 @@ Contributors * Ronald Portier * Ignacio Ibeas * George Daramouskas +* Jan Verbeek + +Other credits +~~~~~~~~~~~~~ ACME implementation -------------------- +~~~~~~~~~~~~~~~~~~~ * https://github.com/certbot/certbot/tree/0.22.x/acme Icon ----- +~~~~ * https://helloworld.letsencrypt.org -Maintainer ----------- +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. .. 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. +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index ed1c796ed57..e3f8f9fdca8 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -1,7 +1,7 @@ # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { - "name": "Let's encrypt", + "name": "Let's Encrypt", "version": "11.0.2.0.0", "author": "Therp BV," "Tecnativa," @@ -16,18 +16,19 @@ "data": [ "data/ir_config_parameter.xml", "data/ir_cron.xml", - "demo/ir_cron.xml", "views/base_config_settings.xml", ], + "demo": [ + "demo/ir_cron.xml", + ], "post_init_hook": "post_init_hook", "installable": True, "external_dependencies": { "python": [ "acme", "cryptography", + "dns", "josepy", - "IPy", - "OpenSSL", ], }, } diff --git a/letsencrypt/data/ir_config_parameter.xml b/letsencrypt/data/ir_config_parameter.xml index e7932e7af14..9637cbfd86f 100644 --- a/letsencrypt/data/ir_config_parameter.xml +++ b/letsencrypt/data/ir_config_parameter.xml @@ -14,7 +14,7 @@ id="letsencrypt_backoff" model="ir.config_parameter" forcecreate="True"> - letsencrypt_backoff + letsencrypt.backoff 3 - Update letsencrypt certificates + Check Let's Encrypt certificates code model._cron() - 11 - weeks + days + 1 -1 diff --git a/letsencrypt/demo/ir_cron.xml b/letsencrypt/demo/ir_cron.xml index e4451aa5946..926e80d367a 100644 --- a/letsencrypt/demo/ir_cron.xml +++ b/letsencrypt/demo/ir_cron.xml @@ -1,8 +1,6 @@ - - - - - - - + + + + + diff --git a/letsencrypt/hooks.py b/letsencrypt/hooks.py index 51477ae97f9..087deaf779d 100644 --- a/letsencrypt/hooks.py +++ b/letsencrypt/hooks.py @@ -5,4 +5,4 @@ def post_init_hook(cr, pool): env = api.Environment(cr, SUPERUSER_ID, {}) - env['letsencrypt']._generate_key('account_key') + env['letsencrypt']._get_key('account.key') diff --git a/letsencrypt/migrations/11.0.2.0.0/post-migrate.py b/letsencrypt/migrations/11.0.2.0.0/post-migrate.py index 0340c51af26..57a6a9c3aa6 100644 --- a/letsencrypt/migrations/11.0.2.0.0/post-migrate.py +++ b/letsencrypt/migrations/11.0.2.0.0/post-migrate.py @@ -4,21 +4,28 @@ def migrate_altnames(env): - ir_config_parameter = env['ir.config_parameter'] - new_domains = ','.join(ir_config_parameter.search([ - ('key', '=like', 'letsencrypt.altname.%')]).mapped('value')) - ir_config_parameter.set_param('letsencrypt_altnames', new_domains) - ir_config_parameter.search([ - ('key', '=like', 'letsencrypt.altname.%')]).unlink() + config = env["ir.config_parameter"] + existing = config.search([("key", "=like", "letsencrypt.altname.%")]) + new_domains = "\n".join(existing.mapped("value")) + config.set_param("letsencrypt.altnames", new_domains) + existing.unlink() def migrate_cron(env): - ir_cron = env['ir.cron'] - old_cron = ir_cron.search([ - ('model', '=', 'letsencrypt'), - ('function', '=', 'cron')]) - if old_cron: - old_cron.write({'function': '_cron'}) + # Any interval that was appropriate for the old version is inappropriate + # for the new one, so it's ok to clobber it. + # But tweaking it afterwards is fine, so noupdate="1" still makes sense. + jobs = ( + env["ir.cron"] + .with_context(active_test=False) + .search([("model", "=", "letsencrypt"), ("function", "=", "cron")]) + ) + if not jobs: + # ir.cron._try_lock doesn't handle empty recordsets well + return + jobs.write( + {"function": "_cron", "interval_type": "days", "interval_number": "1"} + ) def migrate(cr, version): diff --git a/letsencrypt/models/base_config_settings.py b/letsencrypt/models/base_config_settings.py index 4634e2a2fdd..2f89183915f 100644 --- a/letsencrypt/models/base_config_settings.py +++ b/letsencrypt/models/base_config_settings.py @@ -1,75 +1,131 @@ # Copyright 2018 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, fields, models + +from odoo import api, exceptions, fields, models + + +DNS_SCRIPT_DEFAULT = """# Write your script here +# It should create a TXT record of $LETSENCRYPT_DNS_CHALLENGE +# on _acme-challenge.$LETSENCRYPT_DNS_DOMAIN +""" class BaseConfigSettings(models.TransientModel): _inherit = 'base.config.settings' - dns_provider = fields.Selection( - [('shell', 'Shell')], - string='DNS Provider', - help='If we need to respond to a DNS challenge we need to add ' - 'a TXT record on your DNS. If you leave this to Shell ' - 'then you signify to the module that this will be taken ' - 'care off by that script written below. ' - 'Generally new modules that are made ' - 'to support various VPS providers add attributes here.', + letsencrypt_altnames = fields.Text( + string="Domain names", + default='', + help=( + 'Additional domains to include on the CSR. ' + 'Separate with commas or newlines.' + ), ) - script = fields.Text( - 'Script', - help='Write your script that will update your DNS TXT records.', + letsencrypt_dns_provider = fields.Selection( + selection=[('shell', 'Shell script')], + string='DNS provider', + help=( + 'For wildcard certificates we need to add a TXT record on your ' + 'DNS. If you set this to "Shell script" you can enter a shell ' + 'script. Other options can be added by installing additional ' + 'modules.' + ), ) - altnames = fields.Text( - default='', - help='Domains for which you want to include on the CSR ' - 'Separate with commas.', + letsencrypt_dns_shell_script = fields.Text( + string='DNS update script', + help=( + 'Write a shell script that will update your DNS TXT records. ' + 'You can use the $LETSENCRYPT_DNS_CHALLENGE and ' + '$LETSENCRYPT_DNS_DOMAIN variables.' + ), + default=DNS_SCRIPT_DEFAULT, ) - needs_dns_provider = fields.Boolean() - reload_command = fields.Text( - 'Reload Command', + letsencrypt_needs_dns_provider = fields.Boolean() + letsencrypt_reload_command = fields.Text( + string='Server reload command', help='Fill this with the command to restart your web server.', ) + letsencrypt_testing_mode = fields.Boolean( + string='Use testing server', + help=( + "Use the Let's Encrypt staging server, which has higher rate " + "limits but doesn't create valid certificates." + ), + ) + letsencrypt_prefer_dns = fields.Boolean( + string="Prefer DNS validation", + help=( + "Validate through DNS even when HTTP validation is possible. " + "Use this if your Odoo instance isn't publicly accessible.", + ) + ) - @api.onchange('altnames') - def onchange_altnames(self): - if self.altnames: - self.needs_dns_provider = any( - '*.' in altname for altname in self.altnames.split(',')) + @api.onchange('letsencrypt_altnames', 'letsencrypt_prefer_dns') + def letsencrypt_check_dns_required(self): + altnames = self.letsencrypt_altnames or '' + self.letsencrypt_needs_dns_provider = ( + "*." in altnames or self.letsencrypt_prefer_dns + ) @api.model - def default_get(self, field_list): - res = super(BaseConfigSettings, self).default_get(field_list) - ir_config_parameter = self.env['ir.config_parameter'] - res.update({ - 'dns_provider': ir_config_parameter.get_param( - 'letsencrypt_dns_provider'), - 'script': ir_config_parameter.get_param( - 'letsencrypt_script'), - 'altnames': ir_config_parameter.get_param( - 'letsencrypt_altnames'), - 'reload_command': ir_config_parameter.get_param( - 'letsencrypt.reload_command'), - }) + def default_get(self, fields_list): + res = super(BaseConfigSettings, self).default_get(fields_list) + get_param = self.env['ir.config_parameter'].get_param + res.update( + { + 'letsencrypt_dns_provider': get_param( + 'letsencrypt.dns_provider' + ), + 'letsencrypt_dns_shell_script': get_param( + 'letsencrypt.dns_shell_script', DNS_SCRIPT_DEFAULT + ), + 'letsencrypt_altnames': get_param('letsencrypt.altnames', ''), + 'letsencrypt_reload_command': get_param( + 'letsencrypt.reload_command' + ), + 'letsencrypt_needs_dns_provider': ( + '*.' in get_param('letsencrypt.altnames', '') + ), + 'letsencrypt_testing_mode': ( + get_param('letsencrypt.testing_mode', 'False') == 'True' + ), + 'letsencrypt_prefer_dns': ( + get_param('letsencrypt.prefer_dns', 'False') == 'True' + ), + } + ) return res @api.multi def set_dns_provider(self): self.ensure_one() - ir_config_parameter = self.env['ir.config_parameter'] - ir_config_parameter.set_param( - 'letsencrypt_dns_provider', - self.dns_provider) - ir_config_parameter.set_param( - 'letsencrypt_needs_dns_provider', - self.needs_dns_provider) - ir_config_parameter.set_param( - 'letsencrypt_script', - self.script) - ir_config_parameter.set_param( - 'letsencrypt_altnames', - self.altnames) - ir_config_parameter.set_param( - 'letsencrypt.reload_command', - self.reload_command) + self.letsencrypt_check_dns_required() + + if self.letsencrypt_dns_provider == 'shell': + lines = [ + line.strip() + for line in self.letsencrypt_dns_shell_script.split('\n') + ] + if all(line == '' or line.startswith('#') for line in lines): + raise exceptions.ValidationError( + "You didn't write a DNS update script!" + ) + + set_param = self.env['ir.config_parameter'].set_param + set_param('letsencrypt.dns_provider', self.letsencrypt_dns_provider) + set_param( + 'letsencrypt.dns_shell_script', self.letsencrypt_dns_shell_script + ) + set_param('letsencrypt.altnames', self.letsencrypt_altnames) + set_param( + 'letsencrypt.reload_command', self.letsencrypt_reload_command + ) + set_param( + 'letsencrypt.testing_mode', + 'True' if self.letsencrypt_testing_mode else 'False', + ) + set_param( + 'letsencrypt.prefer_dns', + 'True' if self.letsencrypt_prefer_dns else 'False', + ) return True diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 6f61f0969b3..85acdabc39d 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -2,50 +2,77 @@ # © 2016 Antonio Espinosa # © 2018 Ignacio Ibeas # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from datetime import datetime, timedelta -from os.path import join, isdir, isfile -from os import makedirs -from odoo import _, api, models, exceptions -from odoo.tools import config -import logging -import urllib.parse -import subprocess -import requests + import base64 +import collections +import logging import os import re +import subprocess +import time +import urlparse -_logger = logging.getLogger(__name__) +from datetime import datetime, timedelta + +import requests +from odoo import _, api, models +from odoo.exceptions import UserError +from odoo.tools import config + +_logger = logging.getLogger(__name__) try: + import acme.challenges + import acme.client + import acme.crypto_util + import acme.errors + import acme.messages + + from cryptography import x509 from cryptography.hazmat.backends import default_backend - import josepy as jose + from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa - from cryptography.hazmat.primitives import serialization, hashes - from acme import client, crypto_util, errors - from acme.messages import Registration, NewRegistration, \ - RegistrationResource - from acme import challenges - import IPy + + import dns.resolver + + import josepy except ImportError as e: _logger.debug(e) - WILDCARD = '*.' # as defined in the spec DEFAULT_KEY_LENGTH = 4096 TYPE_CHALLENGE_HTTP = 'http-01' TYPE_CHALLENGE_DNS = 'dns-01' -V2_STAGING_DIRECTORY_URL = \ +V2_STAGING_DIRECTORY_URL = ( 'https://acme-staging-v02.api.letsencrypt.org/directory' +) V2_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory' +LOCAL_DOMAINS = { + 'localhost', + 'localhost.localdomain', + 'localhost6', + 'localhost6.localdomain6', + 'ip6-localhost', + 'ip6-loopback', +} + +DNSUpdate = collections.namedtuple( + "DNSUpdate", ("challenge", "domain", "token") +) def _get_data_dir(): - return join(config.options.get('data_dir'), 'letsencrypt') + dir_ = os.path.join(config.options.get('data_dir'), 'letsencrypt') + if not os.path.isdir(dir_): + os.makedirs(dir_) + return dir_ def _get_challenge_dir(): - return join(_get_data_dir(), 'acme-challenge') + dir_ = os.path.join(_get_data_dir(), 'acme-challenge') + if not os.path.isdir(dir_): + os.makedirs(dir_) + return dir_ class Letsencrypt(models.AbstractModel): @@ -53,208 +80,399 @@ class Letsencrypt(models.AbstractModel): _description = 'Abstract model providing functions for letsencrypt' @api.model - def _generate_key(self, key_name): - _logger.info('Generating key ' + str(key_name)) - data_dir = _get_data_dir() - if not isdir(data_dir): - makedirs(data_dir) - key_file = join(data_dir, key_name) - if not isfile(key_file): - _logger.info('Generating a new key') - key_json = jose.JWKRSA(key=jose.ComparableRSAKey( - rsa.generate_private_key( - public_exponent=65537, - key_size=DEFAULT_KEY_LENGTH, - backend=default_backend()))) - key = key_json.key.private_bytes( - serialization.Encoding.PEM, - serialization.PrivateFormat.PKCS8, - serialization.NoEncryption()) - with open(key_file, 'wb') as _file: - _file.write(key) - return key_file + def _generate_key(self): + """Generate an entirely new key.""" + return rsa.generate_private_key( + public_exponent=65537, + key_size=DEFAULT_KEY_LENGTH, + backend=default_backend(), + ).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + @api.model + def _get_key(self, key_name): + """Get a key for a filename, generating if if it doesn't exist.""" + key_file = os.path.join(_get_data_dir(), key_name) + if not os.path.isfile(key_file): + _logger.info("Generating new key %s", key_name) + key_bytes = self._generate_key() + try: + with open(key_file, 'wb') as file_: + os.fchmod(file_.fileno(), 0o600) + file_.write(key_bytes) + except BaseException: + # An incomplete file would block generation of a new one + if os.path.isfile(key_file): + os.remove(key_file) + raise + else: + _logger.info("Getting existing key %s", key_name) + with open(key_file, 'rb') as file_: + key_bytes = file_.read() + return key_bytes @api.model def _validate_domain(self, domain): - local_domains = [ - 'localhost', 'localhost.localdomain', 'localhost6', - 'localhost6.localdomain6' - ] + """Validate that a domain is publicly accessible.""" + if ':' in domain or all( + char.isdigit() or char == '.' for char in domain + ): + raise UserError( + _("Domain %s: Let's Encrypt doesn't support IP addresses!") + % domain + ) - def _ip_is_private(address): - try: - ip = IPy.IP(address) - except ValueError: - return False - return ip.iptype() == 'PRIVATE' + if domain in LOCAL_DOMAINS or '.' not in domain: + raise UserError( + _("Domain %s: Let's encrypt doesn't work with local domains!") + % domain + ) + + @api.model + def _should_run(self, cert_file, domains): + """Inspect the existing certificate to see if action is necessary.""" + domains = set(domains) + + if not os.path.isfile(cert_file): + _logger.info("No existing certificate found, creating a new one") + return True + + with open(cert_file, 'rb') as file_: + cert = x509.load_pem_x509_certificate( + file_.read(), default_backend() + ) + expiry = cert.not_valid_after + remaining = expiry - datetime.now() + if remaining < timedelta(): + _logger.warning( + "Certificate expired on %s, which was %d days ago!", + expiry, + -remaining.days, + ) + _logger.info("Renewing certificate now.") + return True + if remaining < timedelta(days=30): + _logger.info( + "Certificate expires on %s, which is in %d days, renewing it", + expiry, + remaining.days, + ) + return True + + # Should be a single name, but this is how the API works + names = { + entry.value + for entry in cert.subject.get_attributes_for_oid( + x509.oid.NameOID.COMMON_NAME + ) + } + try: + names.update( + cert.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME + ).value.get_values_for_type(x509.DNSName) + ) + except x509.extensions.ExtensionNotFound: + pass - if domain in local_domains or _ip_is_private(domain): - raise exceptions.Warning( - _("Let's encrypt doesn't work with private addresses " - "or local domains!")) + missing = domains - names + if missing: + _logger.info( + "Found new domains %s, requesting new certificate", + ', '.join(missing), + ) + return True + + _logger.info( + "Certificate expires on %s, which is in %d days, no action needed", + expiry, + remaining.days, + ) + return False @api.model def _cron(self): ir_config_parameter = self.env['ir.config_parameter'] - domain = urllib.parse.urlparse( - self.env['ir.config_parameter'].get_param( - 'web.base.url', 'localhost')).netloc - self._validate_domain(domain) - # Generate account key - account_key_file = self._generate_key('account.key') - account_key = jose.JWKRSA.load(open(account_key_file).read()) - # Generate domain key - domain_key_file = self._generate_key(domain) + base_url = ir_config_parameter.get_param('web.base.url', 'localhost') + domain = urlparse.urlparse(base_url).hostname + cert_file = os.path.join(_get_data_dir(), '%s.crt' % domain) + + domains = self._cascade_domains([domain] + self._get_altnames()) + for dom in domains: + self._validate_domain(dom) + + if not self._should_run(cert_file, domains): + return + + account_key = josepy.JWKRSA.load(self._get_key('account.key')) + domain_key = self._get_key('%s.key' % domain) + client = self._create_client(account_key) - new_reg = NewRegistration( - key=account_key.public_key(), - terms_of_service_agreed=True) + new_reg = acme.messages.NewRegistration( + key=account_key.public_key(), terms_of_service_agreed=True + ) try: client.new_account(new_reg) - except errors.ConflictError as e: - reg = Registration(key=account_key.public_key()) - reg_res = RegistrationResource( - body=reg, - uri=e.location, + _logger.info("Successfully registered.") + except acme.errors.ConflictError as err: + reg = acme.messages.Registration(key=account_key.public_key()) + reg_res = acme.messages.RegistrationResource( + body=reg, uri=err.location ) client.query_registration(reg_res) - csr = self._make_csr(account_key, domain_key_file, domain) + _logger.info("Reusing existing account.") + + _logger.info('Making CSR for the following domains: %s', domains) + csr = acme.crypto_util.make_csr( + private_key_pem=domain_key, domains=domains + ) authzr = client.new_order(csr) - auth_responded = False + + # For each requested domain name we receive a list of challenges. + # We only have to do one from each list. + # HTTP challenges are the easiest, so do one of those if possible. + # We can do DNS challenges too. There are other types that we don't + # support. + pending_responses = [] + + prefer_dns = ( + self.env["ir.config_parameter"].get_param("letsencrypt.prefer_dns") + == "True" + ) for authorizations in authzr.authorizations: - for challenge in sorted( - authorizations.body.challenges, - key=lambda x: x.chall.typ == TYPE_CHALLENGE_HTTP, - reverse=True): + http_challenges = [ + challenge + for challenge in authorizations.body.challenges + if challenge.chall.typ == TYPE_CHALLENGE_HTTP + ] + other_challenges = [ + challenge + for challenge in authorizations.body.challenges + if challenge.chall.typ != TYPE_CHALLENGE_HTTP + ] + if prefer_dns: + ordered_challenges = other_challenges + http_challenges + else: + ordered_challenges = http_challenges + other_challenges + for challenge in ordered_challenges: if challenge.chall.typ == TYPE_CHALLENGE_HTTP: self._respond_challenge_http(challenge, account_key) client.answer_challenge( - challenge, - challenges.HTTP01Response()) - auth_responded = True + challenge, acme.challenges.HTTP01Response() + ) break elif challenge.chall.typ == TYPE_CHALLENGE_DNS: - self._respond_challenge_dns( - challenge, - account_key, - authorizations.body.identifier.value, + domain = authorizations.body.identifier.value + token = challenge.validation(account_key) + self._respond_challenge_dns(domain, token) + # We delay this because we wait for each domain. + # That takes less time if they've all already been changed. + pending_responses.append( + DNSUpdate( + challenge=challenge, domain=domain, token=token + ) ) - client.answer_challenge( - challenge, challenges.DNSResponse()) - auth_responded = True break - if not auth_responded: - raise exceptions.ValidationError( - _('Could not respond to letsencrypt challenges.')) + else: + raise UserError( + _('Could not respond to letsencrypt challenges.') + ) + + if pending_responses: + for update in pending_responses: + self._wait_for_record(update.domain, update.token) + # 1 minute was not always enough during testing, even once records + # were visible locally + _logger.info( + "All TXT records found, waiting 5 minutes more to make sure." + ) + time.sleep(300) + for update in pending_responses: + client.answer_challenge( + update.challenge, acme.challenges.DNSResponse() + ) + # let them know we are done and they should check - deadline = datetime.now() + timedelta( - minutes=int( - ir_config_parameter.get_param('letsencrypt_backoff', 3))) - order_resource = client.poll_and_finalize(authzr, deadline) - with open(join(_get_data_dir(), '%s.crt' % domain), 'w') as crt: + backoff = int(ir_config_parameter.get_param('letsencrypt.backoff', 3)) + deadline = datetime.now() + timedelta(minutes=backoff) + try: + order_resource = client.poll_and_finalize(authzr, deadline) + except acme.errors.ValidationError as error: + _logger.error("Let's Encrypt validation failed!") + for authz in error.failed_authzrs: + for challenge in authz.body.challenges: + _logger.error(str(challenge.error)) + raise + + with open(cert_file, 'w') as crt: crt.write(order_resource.fullchain_pem) - _logger.info('SUCCESS: Certificate saved :%s', crt.name) - reload_cmd = ir_config_parameter.get_param( - 'letsencrypt.reload_command', False) - if reload_cmd: - self._call_cmdline(['sh', '-c', reload_cmd]) + _logger.info('SUCCESS: Certificate saved: %s', cert_file) + reload_cmd = ir_config_parameter.get_param( + 'letsencrypt.reload_command', '' + ) + if reload_cmd.strip(): + self._call_cmdline(reload_cmd) + else: + _logger.warning("No reload command defined.") + + @api.model + def _wait_for_record(self, domain, token): + """Wait until a TXT record for a domain is visible.""" + if not domain.endswith("."): + # Fully qualify domain name, or it may try unsuitable names too + domain += "." + attempt = 0 + while True: + attempt += 1 + try: + for record in dns.resolver.query( + "_acme-challenge." + domain, "TXT" + ): + value = record.to_text()[1:-1] + if value == token: + return + else: + _logger.debug("Found %r instead of %r", value, token) + except dns.resolver.NXDOMAIN: + _logger.debug("Record for %r does not exist yet", domain) + if attempt < 30: + _logger.info("Waiting for DNS update.") + time.sleep(60) else: - _logger.warning("No reload command defined.") + _logger.warning( + "Could not find new record after 30 minutes! " + "Giving up and hoping for the best." + ) + return + @api.model def _create_client(self, account_key): - net = client.ClientNetwork(account_key) - if config['test_enable']: + param = self.env['ir.config_parameter'] + testing_mode = param.get_param('letsencrypt.testing_mode') == 'True' + if config['test_enable'] or testing_mode: directory_url = V2_STAGING_DIRECTORY_URL else: directory_url = V2_DIRECTORY_URL directory_json = requests.get(directory_url).json() - return client.ClientV2(directory_json, net) - - def _cascade_domains(self, altnames): - """ Given an list of domains containing one or more wildcard domains - the following are performed: - 1) for every wildcard domain: - a) gets the index of it's wildcard characters - b) if there are non wildcard domain names that are the same - after the index of the current wildcard name remove them. - 2) when done, return the modified altnames + net = acme.client.ClientNetwork(account_key) + return acme.client.ClientV2(directory_json, net) + + @api.model + def _cascade_domains(self, domains): + """Remove domains that are obsoleted by wildcard domains in the list. + + Requesting www.example.com is unnecessary if *.example.com is also + requested. example.com isn't obsoleted however, and neither is + sub.domain.example.com. """ - for altname in filter(lambda x: WILDCARD in x, altnames): - pat = re.compile('^.*' + altname.replace(WILDCARD, '') + '.*$') - for _altname in filter(lambda x: WILDCARD not in x, altnames): - if pat.search(_altname): - altnames.remove(_altname) - return altnames - - def _make_csr(self, account_key, domain_key_file, domain): - parameter_model = self.env['ir.config_parameter'] - altnames = parameter_model.get_param('letsencrypt_altnames') - if altnames: - altnames = re.split(',|\n| |;', altnames) - valid_domains = altnames + [domain] - valid_domains = self._cascade_domains(valid_domains) - else: - valid_domains = [domain] - _logger.info( - 'Making CSR for the following domains: ' + str(valid_domains)) - return crypto_util.make_csr( - open(domain_key_file).read(), valid_domains) + to_remove = set() + for domain in domains: + if WILDCARD in domain[1:]: + raise UserError( + _("A wildcard is only allowed at the start of a domain") + ) + if domain.startswith(WILDCARD): + postfix = domain[1:] # e.g. ".example.com" + # This makes it O(n²) but n <= 100 so it's ok + for other in domains: + if other.startswith(WILDCARD): + continue + if other.endswith(postfix): + prefix = other[: -len(postfix)] # e.g. "www" + if '.' not in prefix: + to_remove.add(other) + return sorted(set(domains) - to_remove) + + @api.model + def _get_altnames(self): + """Get the configured altnames as a list of strings.""" + altnames = self.env['ir.config_parameter'].get_param( + 'letsencrypt.altnames' + ) + if not altnames: + return [] + return re.split('(?:,|\n| |;)+', altnames) + + @api.model def _respond_challenge_http(self, challenge, account_key): """ Respond to the HTTP challenge by writing the file to serve. """ - challenge_dir = _get_challenge_dir() - if not isdir(challenge_dir): - makedirs(challenge_dir) - token = base64.urlsafe_b64encode(challenge.token) - challenge_file = join(_get_challenge_dir(), '%s' % token.rstrip('=')) - with open(challenge_file, 'wb') as challenge_file: - challenge_file.write(token.rstrip('=') + '.' + jose.b64encode( - account_key.thumbprint(hash_function=hashes.SHA256)).decode()) - - def _respond_challenge_dns(self, challenge, account_key, domain): + token = self._base64_encode(challenge.token) + challenge_file = os.path.join(_get_challenge_dir(), token.decode()) + with open(challenge_file, 'w') as file_: + file_.write(challenge.validation(account_key)) + + @api.model + def _respond_challenge_dns(self, domain, token): """ Respond to the DNS challenge by creating the DNS record on the provider. """ - letsencrypt_dns_function = '_respond_challenge_dns_' + \ - self.env['ir.config_parameter'].get_param( - 'letsencrypt_dns_provider') - getattr(self, letsencrypt_dns_function)(challenge, account_key, domain) + provider = self.env['ir.config_parameter'].get_param( + 'letsencrypt.dns_provider' + ) + if not provider: + raise UserError( + _("No DNS provider set, can't request wildcard certificate") + ) + dns_function = getattr(self, "_respond_challenge_dns_" + provider) + dns_function(domain.replace("*.", ""), token) @api.model - def _call_cmdline(self, cmdline, env=None, shell=False): + def _call_cmdline(self, cmdline, env=None): + """Call a shell command.""" process = subprocess.Popen( cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, - shell=shell, + shell=True, ) stdout, stderr = process.communicate() + stdout = stdout.strip() + stderr = stderr.strip() if process.returncode: - raise exceptions.Warning(_( - 'Error calling %s: %s %s %s' % ( - cmdline, - str(process.returncode) + ' '.join(cmdline), - stdout, - stderr, - ))) + if stdout: + _logger.warning(stdout) + if stderr: + _logger.warning(stderr) + raise UserError( + _('Error calling %s: %d') % (cmdline, process.returncode) + ) + if stdout: + _logger.info(stdout) + if stderr: + _logger.info(stderr) @api.model - def _respond_challenge_dns_shell(self, challenge, account_key, domain): + def _respond_challenge_dns_shell(self, domain, token): + """Respond to a DNS challenge using an arbitrary shell command.""" script_str = self.env['ir.config_parameter'].get_param( - 'letsencrypt_script') + 'letsencrypt.dns_shell_script' + ) if script_str: - env = os.environ + env = os.environ.copy() env.update( - LETSENCRYPT_DNS_CHALLENGE=jose.encode_b64jose( - challenge.chall.token), LETSENCRYPT_DNS_DOMAIN=domain, + LETSENCRYPT_DNS_CHALLENGE=token, ) - self.env['letsencrypt']._call_cmdline( - script_str, - env=env, - shell=True, + self._call_cmdline(script_str, env=env) + else: + raise UserError( + _("No shell command configured for updating DNS records") ) + + @api.model + def _base64_encode(self, data): + """Encode data as a URL-safe base64 string without padding. + + This should be the encoding that Let's Encrypt uses for all base64. See + https://github.com/ietf-wg-acme/acme/issues/64#issuecomment-168852757 + and https://golang.org/pkg/encoding/base64/#RawURLEncoding + """ + return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii') diff --git a/letsencrypt/readme/CONFIGURE.rst b/letsencrypt/readme/CONFIGURE.rst new file mode 100644 index 00000000000..0b07a720ebf --- /dev/null +++ b/letsencrypt/readme/CONFIGURE.rst @@ -0,0 +1,24 @@ +This addons requests a certificate for the domain named in the configuration +parameter ``web.base.url`` - if this comes back as ``localhost`` or the like, +the module doesn't request anything. + +Futher self-explanatory settings are in Settings -> General Settings. There you +can add further domains to the CSR, add a custom script that updates your DNS +and add a script that will be used to reload your web server (if needed). +The number of domains that can be added to a certificate is +`capped at 100 `_. A wildcard +certificate can be used to avoid that limit. + +Note that all those domains must be publicly reachable on port 80 via HTTP, and +they must have an entry for ``.well-known/acme-challenge`` pointing to +``$datadir/letsencrypt/acme-challenge`` of your odoo instance. + +Since DNS changes can take some time to propagate, when we respond to a DNS challenge +and the server tries to check our response, it might fail (and probably will). +The solution to this is documented in https://tools.ietf.org/html/rfc8555#section-8.2 +and basically is a ``Retry-After`` header under which we can instruct the server to +retry the challenge. +At the time these lines were written, Boulder had not implemented this functionality. +This prompted us to use ``letsencrypt.backoff`` configuration parameter, which is the +amount of minutes this module will try poll the server to retry validating the answer +to our challenge, specifically it is the ``deadline`` parameter of ``poll_and_finalize``. diff --git a/letsencrypt/readme/CONTRIBUTORS.rst b/letsencrypt/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..5da59e4d396 --- /dev/null +++ b/letsencrypt/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* Holger Brunn +* Antonio Espinosa +* Dave Lasley +* Ronald Portier +* George Daramouskas +* Jan Verbeek diff --git a/letsencrypt/readme/CREDITS.rst b/letsencrypt/readme/CREDITS.rst new file mode 100644 index 00000000000..cea29d7411e --- /dev/null +++ b/letsencrypt/readme/CREDITS.rst @@ -0,0 +1,9 @@ +ACME implementation +~~~~~~~~~~~~~~~~~~~ + +* https://github.com/certbot/certbot/tree/0.22.x/acme + +Icon +~~~~ + +* https://helloworld.letsencrypt.org diff --git a/letsencrypt/readme/DESCRIPTION.rst b/letsencrypt/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..4f0be24b106 --- /dev/null +++ b/letsencrypt/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module was written to have your Odoo installation request SSL certificates +from https://letsencrypt.org automatically. diff --git a/letsencrypt/readme/INSTALL.rst b/letsencrypt/readme/INSTALL.rst new file mode 100644 index 00000000000..5e35d3b7e99 --- /dev/null +++ b/letsencrypt/readme/INSTALL.rst @@ -0,0 +1,10 @@ +After installation, this module generates a private key for your account at +letsencrypt.org automatically in ``$data_dir/letsencrypt/account.key``. If you +want or need to use your own account key, replace the file. + +For certificate requests to work, your site needs to be accessible via plain +HTTP, see below for configuration examples in case you force your clients to +the SSL version. + +After installation, trigger the cronjob `Update letsencrypt certificates` and +watch your log for messages. diff --git a/letsencrypt/readme/USAGE.rst b/letsencrypt/readme/USAGE.rst new file mode 100644 index 00000000000..b26fe486a22 --- /dev/null +++ b/letsencrypt/readme/USAGE.rst @@ -0,0 +1,63 @@ +The module sets up a cronjob that requests and renews certificates automatically. + +Certificates are renewed a month before they expire. Renewal is then attempted +every day until it succeeds. + +After the first run, you'll find a file called ``domain.crt`` in +``$datadir/letsencrypt``, configure your SSL proxy to use this file as certificate. + +In depth configuration +~~~~~~~~~~~~~~~~~~~~~~ + +If you want to use multiple domains on your CSR then you have to configure them +from Settings -> General Settings. If you use a wildcard in any of those domains +then letsencrypt will return a DNS challenge. In order for that challenge to be +answered you will need to **either** provide a script (as seen in General Settings) +or install a module that provides support for your DNS provider. In that module +you will need to create a function in the letsencrypt model with the name +``_respond_challenge_dns_$DNS_PROVIDER`` where ``$DNS_PROVIDER`` is the name of your +provider and can be any string with length greater than zero, and add the name +of your DNS provider in the settings dns_provider selection field. + +In any case if a script path is inserted in the settings page, it will be run +in case you want to update multiple DNS servers. + +A reload command can be set in the Settings as well in case you need to reload +your web server. This by default is ``sudo /usr/sbin/service nginx reload`` + + +You'll also need a matching sudo configuration, like:: + + your_odoo_user ALL = NOPASSWD: /usr/sbin/service nginx reload + +Further, if you force users to https, you'll need something like for nginx:: + + if ($scheme = "http") { + set $redirect_https 1; + } + if ($request_uri ~ ^/.well-known/acme-challenge/) { + set $redirect_https 0; + } + if ($redirect_https) { + rewrite ^ https://$server_name$request_uri? permanent; + } + +and this for apache:: + + RewriteEngine On + RewriteCond %{HTTPS} !=on + RewriteCond %{REQUEST_URI} "!^/.well-known/" + RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L] + +In case you need to redirect other nginx sites to your Odoo instance, declare +an upstream for your odoo instance and do something like:: + + location /.well-known { + proxy_pass http://yourodooupstream; + } + +If you're using a multi-database installation (with or without dbfilter option) +where /web/databse/selector returns a list of more than one database, then +you need to add ``letsencrypt`` addon to wide load addons list +(by default, only ``web`` addon), setting ``--load`` option. +For example, ``--load=web,letsencrypt`` diff --git a/letsencrypt/static/description/index.html b/letsencrypt/static/description/index.html new file mode 100644 index 00000000000..7706bfc69a0 --- /dev/null +++ b/letsencrypt/static/description/index.html @@ -0,0 +1,544 @@ + + + + + + +Let's Encrypt + + + +
+

Let’s Encrypt

+ + +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

+

This module was written to have your Odoo installation request SSL certificates +from https://letsencrypt.org automatically.

+

Table of contents

+ +
+

Installation

+

After installation, this module generates a private key for your account at +letsencrypt.org automatically in $data_dir/letsencrypt/account.key. If you +want or need to use your own account key, replace the file.

+

For certificate requests to work, your site needs to be accessible via plain +HTTP, see below for configuration examples in case you force your clients to +the SSL version.

+

After installation, trigger the cronjob Update letsencrypt certificates and +watch your log for messages.

+
+
+

Configuration

+

This addons requests a certificate for the domain named in the configuration +parameter web.base.url - if this comes back as localhost or the like, +the module doesn’t request anything.

+

Futher self-explanatory settings are in Settings -> General Settings. There you +can add further domains to the CSR, add a custom script that updates your DNS +and add a script that will be used to reload your web server (if needed). +The number of domains that can be added to a certificate is +capped at 100. A wildcard +certificate can be used to avoid that limit.

+

Note that all those domains must be publicly reachable on port 80 via HTTP, and +they must have an entry for .well-known/acme-challenge pointing to +$datadir/letsencrypt/acme-challenge of your odoo instance.

+

Since DNS changes can take some time to propagate, when we respond to a DNS challenge +and the server tries to check our response, it might fail (and probably will). +The solution to this is documented in https://tools.ietf.org/html/rfc8555#section-8.2 +and basically is a Retry-After header under which we can instruct the server to +retry the challenge. +At the time these lines were written, Boulder had not implemented this functionality. +This prompted us to use letsencrypt.backoff configuration parameter, which is the +amount of minutes this module will try poll the server to retry validating the answer +to our challenge, specifically it is the deadline parameter of poll_and_finalize.

+
+
+

Usage

+

The module sets up a cronjob that requests and renews certificates automatically.

+

Certificates are renewed a month before they expire. Renewal is then attempted +every day until it succeeds.

+

After the first run, you’ll find a file called domain.crt in +$datadir/letsencrypt, configure your SSL proxy to use this file as certificate.

+
+

In depth configuration

+

If you want to use multiple domains on your CSR then you have to configure them +from Settings -> General Settings. If you use a wildcard in any of those domains +then letsencrypt will return a DNS challenge. In order for that challenge to be +answered you will need to either provide a script (as seen in General Settings) +or install a module that provides support for your DNS provider. In that module +you will need to create a function in the letsencrypt model with the name +_respond_challenge_dns_$DNS_PROVIDER where $DNS_PROVIDER is the name of your +provider and can be any string with length greater than zero, and add the name +of your DNS provider in the settings dns_provider selection field.

+

In any case if a script path is inserted in the settings page, it will be run +in case you want to update multiple DNS servers.

+

A reload command can be set in the Settings as well in case you need to reload +your web server. This by default is sudo /usr/sbin/service nginx reload

+

You’ll also need a matching sudo configuration, like:

+
+your_odoo_user ALL = NOPASSWD: /usr/sbin/service nginx reload
+
+

Further, if you force users to https, you’ll need something like for nginx:

+
+if ($scheme = "http") {
+    set $redirect_https 1;
+}
+if ($request_uri ~ ^/.well-known/acme-challenge/) {
+    set $redirect_https 0;
+}
+if ($redirect_https) {
+    rewrite ^   https://$server_name$request_uri? permanent;
+}
+
+

and this for apache:

+
+RewriteEngine On
+RewriteCond %{HTTPS} !=on
+RewriteCond %{REQUEST_URI} "!^/.well-known/"
+RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
+
+

In case you need to redirect other nginx sites to your Odoo instance, declare +an upstream for your odoo instance and do something like:

+
+location /.well-known {
+    proxy_pass    http://yourodooupstream;
+}
+
+

If you’re using a multi-database installation (with or without dbfilter option) +where /web/databse/selector returns a list of more than one database, then +you need to add letsencrypt addon to wide load addons list +(by default, only web addon), setting --load option. +For example, --load=web,letsencrypt

+
+
+
+

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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Therp BV
  • +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+ + + +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index a9fce6f410a..29226491b5a 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -1,102 +1,333 @@ # Copyright 2018 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo.tests import SingleTransactionCase -from ..models.letsencrypt import _get_data_dir, WILDCARD -from os import path + +from __future__ import unicode_literals # cryptography is picky + +import os import shutil + +from datetime import datetime, timedelta +from os import path + import mock -import os +from odoo.exceptions import UserError +from odoo.tests import SingleTransactionCase -class TestLetsencrypt(SingleTransactionCase): +from ..models.letsencrypt import _get_data_dir, _get_challenge_dir + + +CERT_DIR = path.join(path.dirname(__file__), 'certs') + + +def _poll(order, deadline): + order_resource = mock.Mock(['fullchain_pem']) + order_resource.fullchain_pem = 'chain' + return order_resource - post_install = True - at_install = False +class TestLetsencrypt(SingleTransactionCase): def setUp(self): super(TestLetsencrypt, self).setUp() + self.env['ir.config_parameter'].set_param( + 'web.base.url', 'http://www.example.com' + ) + self.env['base.config.settings'].create( + { + 'letsencrypt_dns_provider': 'shell', + 'letsencrypt_dns_shell_script': 'touch /tmp/.letsencrypt_test', + 'letsencrypt_altnames': '*.example.com', + 'letsencrypt_reload_command': 'true', # i.e. /bin/true + } + ).set_dns_provider() def test_config_settings(self): - settings_model = self.env['base.config.settings'] - letsencrypt_model = self.env['letsencrypt'] - settings = settings_model.create({ - 'dns_provider': 'shell', - 'script': 'touch /tmp/.letsencrypt_test', - 'altnames': - 'test.example.com', - 'reload_command': 'echo', - }) - self.env['ir.config_parameter'].set_param( - 'web.base.url', 'http://www.example.com') - settings.set_dns_provider() - setting_vals = settings.default_get([]) - self.assertEquals(setting_vals['dns_provider'], 'shell') - letsencrypt_model._call_cmdline(setting_vals['script'], shell=True) - self.assertEquals(path.exists('/tmp/.letsencrypt_test'), True) - self.assertEquals( - setting_vals['altnames'], - settings.altnames, - ) - self.assertEquals(setting_vals['reload_command'], 'echo') - settings.onchange_altnames() - self.assertEquals(settings.needs_dns_provider, False) - settings.unlink() - - def new_order(self, typ): - if typ not in ['http-01', 'dns-01']: - raise ValueError - authzr = mock.Mock - order_resource = mock.Mock - order_resource.fullchain_pem = 'test' - authorization = mock.Mock - body = mock.Mock - challenge = mock.Mock - challenge.chall = mock.Mock - challenge.chall.typ = typ - challenge.chall.token = 'a_token' - body.challenges = [challenge] - authorization.body = body - authzr.authorizations = [authorization] - return authzr - - def poll(self, deadline): - order_resource = mock.Mock - order_resource.fullchain_pem = 'chain' - return order_resource + setting_vals = self.env['base.config.settings'].default_get([]) + self.assertEqual(setting_vals['letsencrypt_dns_provider'], 'shell') + self.assertEqual( + setting_vals['letsencrypt_dns_shell_script'], + 'touch /tmp/.letsencrypt_test', + ) + self.assertEqual(setting_vals['letsencrypt_altnames'], '*.example.com') + self.assertEqual(setting_vals['letsencrypt_reload_command'], 'true') + self.assertTrue(setting_vals['letsencrypt_needs_dns_provider']) + self.assertFalse(setting_vals['letsencrypt_prefer_dns']) @mock.patch('acme.client.ClientV2.answer_challenge') - @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=poll) + @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=_poll) def test_http_challenge(self, poll, answer_challenge): letsencrypt = self.env['letsencrypt'] - self.env['ir.config_parameter'].set_param( - 'web.base.url', 'http://www.example.com') - settings = self.env['base.config.settings'] - settings.create({ - 'altnames': 'test.example.com', - }).set_dns_provider() + self.env['base.config.settings'].create( + {'letsencrypt_altnames': 'test.example.com'} + ).set_dns_provider() letsencrypt._cron() poll.assert_called() + self.assertTrue(os.listdir(_get_challenge_dir())) + self.assertFalse(path.isfile('/tmp/.letsencrypt_test')) self.assertTrue( - open(path.join(_get_data_dir(), 'www.example.com.crt')).read(), + path.isfile(path.join(_get_data_dir(), 'www.example.com.crt')) ) - os.remove(path.join(_get_data_dir(), 'www.example.com.crt')) + # pylint: disable=unused-argument + @mock.patch('odoo.addons.letsencrypt.models.letsencrypt.DNSUpdate') + @mock.patch('dns.resolver.query') + @mock.patch('time.sleep') @mock.patch('acme.client.ClientV2.answer_challenge') - @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=poll) - def test_dns_challenge(self, poll, answer_challenge): - letsencrypt = self.env['letsencrypt'] - settings = self.env['base.config.settings'] - settings.create({ - 'dns_provider': 'shell', - 'script': 'echo', - 'altnames': WILDCARD + 'example.com', - }).set_dns_provider() - letsencrypt._cron() + @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=_poll) + def test_dns_challenge(self, poll, answer_challenge, sleep, query, dnsupd): + + def register_update(challenge, domain, token): + record = mock.Mock() + record.to_text.return_value = '"%s"' % token + query.return_value = [record] + ret = mock.Mock() + ret.challenge = challenge + ret.domain = domain + ret.token = token + return ret + + dnsupd.side_effect = register_update + + self.install_certificate(days_left=10) + self.env['letsencrypt']._cron() poll.assert_called() + query.assert_called_with("_acme-challenge.example.com.", "TXT") + self.assertTrue(path.isfile('/tmp/.letsencrypt_test')) self.assertTrue( - open(path.join(_get_data_dir(), 'www.example.com.crt')).read(), + path.isfile(path.join(_get_data_dir(), 'www.example.com.crt')) + ) + + def test_dns_challenge_error_on_missing_provider(self): + self.env['base.config.settings'].create( + { + 'letsencrypt_altnames': '*.example.com', + 'letsencrypt_dns_provider': False, + } + ).set_dns_provider() + with self.assertRaises(UserError): + self.env['letsencrypt']._cron() + + def test_prefer_dns_setting(self): + self.env['base.config.settings'].create( + { + 'letsencrypt_altnames': 'example.com', + 'letsencrypt_prefer_dns': True, + } + ).set_dns_provider() + self.env['ir.config_parameter'].set_param( + 'web.base.url', 'http://example.com' + ) + # pylint: disable=no-value-for-parameter + self.test_dns_challenge() + + def test_cascading(self): + cascade = self.env['letsencrypt']._cascade_domains + self.assertEqual( + cascade( + [ + 'www.example.com', + '*.example.com', + 'example.com', + 'example.com', + 'notexample.com', + 'multi.sub.example.com', + 'www2.example.com', + 'unrelated.com', + ] + ), + [ + '*.example.com', + 'example.com', + 'multi.sub.example.com', + 'notexample.com', + 'unrelated.com', + ], + ) + self.assertEqual(cascade([]), []) + self.assertEqual(cascade(['*.example.com']), ['*.example.com']) + self.assertEqual(cascade(['www.example.com']), ['www.example.com']) + self.assertEqual( + cascade(['www.example.com', 'example.com']), + ['example.com', 'www.example.com'], ) + with self.assertRaises(UserError): + cascade(['www.*.example.com']) + + with self.assertRaises(UserError): + cascade(['*.*.example.com']) + + def test_altnames_parsing(self): + config = self.env['ir.config_parameter'] + letsencrypt = self.env['letsencrypt'] + + self.assertEqual(letsencrypt._get_altnames(), ['*.example.com']) + + config.set_param('letsencrypt.altnames', '') + self.assertEqual(letsencrypt._get_altnames(), []) + + config.set_param('letsencrypt.altnames', 'www.example.com') + self.assertEqual(letsencrypt._get_altnames(), ['www.example.com']) + + config.set_param( + 'letsencrypt.altnames', 'example.com,example.org,example.net' + ) + self.assertEqual( + letsencrypt._get_altnames(), + ['example.com', 'example.org', 'example.net'], + ) + + config.set_param( + 'letsencrypt.altnames', 'example.com, example.org\nexample.net' + ) + self.assertEqual( + letsencrypt._get_altnames(), + ['example.com', 'example.org', 'example.net'], + ) + + def test_key_generation_and_retrieval(self): + key_a1 = self.env['letsencrypt']._get_key('a.key') + key_a2 = self.env['letsencrypt']._get_key('a.key') + key_b = self.env['letsencrypt']._get_key('b.key') + self.assertIsInstance(key_a1, bytes) + self.assertIsInstance(key_a2, bytes) + self.assertIsInstance(key_b, bytes) + self.assertTrue(path.isfile(path.join(_get_data_dir(), 'a.key'))) + self.assertEqual(key_a1, key_a2) + self.assertNotEqual(key_a1, key_b) + + @mock.patch('os.remove', side_effect=os.remove) + @mock.patch( + 'odoo.addons.letsencrypt.models.letsencrypt.Letsencrypt._generate_key', + side_effect=lambda: None, + ) + def test_interrupted_key_writing(self, generate_key, remove): + with self.assertRaises(TypeError): + self.env['letsencrypt']._get_key('a.key') + self.assertFalse(path.isfile(path.join(_get_data_dir(), 'a.key'))) + remove.assert_called() + generate_key.assert_called() + + def test_domain_validation(self): + self.env['letsencrypt']._validate_domain('example.com') + self.env['letsencrypt']._validate_domain('www.example.com') + + with self.assertRaises(UserError): + self.env['letsencrypt']._validate_domain('1.1.1.1') + with self.assertRaises(UserError): + self.env['letsencrypt']._validate_domain('192.168.1.1') + with self.assertRaises(UserError): + self.env['letsencrypt']._validate_domain('localhost.localdomain') + with self.assertRaises(UserError): + self.env['letsencrypt']._validate_domain('testdomain') + with self.assertRaises(UserError): + self.env['letsencrypt']._validate_domain('::1') + + def test_young_certificate(self): + self.install_certificate(60) + self.assertFalse( + self.env['letsencrypt']._should_run( + path.join(_get_data_dir(), 'www.example.com.crt'), + ['www.example.com', '*.example.com'], + ) + ) + + def test_old_certificate(self): + self.install_certificate(20) + self.assertTrue( + self.env['letsencrypt']._should_run( + path.join(_get_data_dir(), 'www.example.com.crt'), + ['www.example.com', '*.example.com'], + ) + ) + + def test_expired_certificate(self): + self.install_certificate(-10) + self.assertTrue( + self.env['letsencrypt']._should_run( + path.join(_get_data_dir(), 'www.example.com.crt'), + ['www.example.com', '*.example.com'], + ) + ) + + def test_missing_certificate(self): + self.assertTrue( + self.env['letsencrypt']._should_run( + path.join(_get_data_dir(), 'www.example.com.crt'), + ['www.example.com', '*.example.com'], + ) + ) + + def test_new_altnames(self): + self.install_certificate(60, 'www.example.com', ()) + self.assertTrue( + self.env['letsencrypt']._should_run( + path.join(_get_data_dir(), 'www.example.com.crt'), + ['www.example.com', '*.example.com'], + ) + ) + + def test_legacy_certificate_without_altnames(self): + self.install_certificate(60, use_altnames=False) + self.assertFalse( + self.env['letsencrypt']._should_run( + path.join(_get_data_dir(), 'www.example.com.crt'), + ['www.example.com'], + ) + ) + + def install_certificate( + self, + days_left, + common_name='www.example.com', + altnames=('*.example.com',), + use_altnames=True, + ): + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + not_after = datetime.now() + timedelta(days=days_left) + not_before = not_after - timedelta(days=90) + + key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ) + cert_builder = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [x509.NameAttribute(x509.NameOID.COMMON_NAME, common_name)] + ) + ) + .issuer_name( + x509.Name( + [x509.NameAttribute(x509.NameOID.COMMON_NAME, 'myca.biz')] + ) + ) + .not_valid_before(not_before) + .not_valid_after(not_after) + .serial_number(x509.random_serial_number()) + .public_key(key.public_key()) + ) + + if use_altnames: + cert_builder = cert_builder.add_extension( + x509.SubjectAlternativeName( + [x509.DNSName(common_name)] + + [x509.DNSName(name) for name in altnames] + ), + critical=False, + ) + + cert = cert_builder.sign(key, hashes.SHA256(), default_backend()) + cert_file = path.join(_get_data_dir(), '%s.crt' % common_name) + with open(cert_file, 'wb') as file_: + file_.write(cert.public_bytes(serialization.Encoding.PEM)) + def tearDown(self): super(TestLetsencrypt, self).tearDown() shutil.rmtree(_get_data_dir(), ignore_errors=True) + if path.isfile('/tmp/.letsencrypt_test'): + os.remove('/tmp/.letsencrypt_test') diff --git a/letsencrypt/views/base_config_settings.xml b/letsencrypt/views/base_config_settings.xml index 6637a7cafce..93402ee1ebb 100644 --- a/letsencrypt/views/base_config_settings.xml +++ b/letsencrypt/views/base_config_settings.xml @@ -1,5 +1,4 @@ - Letsencrypt base config settings base.config.settings @@ -7,27 +6,28 @@ - + + name="letsencrypt_dns_provider" + attrs="{'required': + [('letsencrypt_needs_dns_provider', '=', True)]}" /> - - + + + + - From f630486a4d18c195080e479ace7d621c3c8bf38a Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Wed, 22 Apr 2020 19:39:48 +0200 Subject: [PATCH 13/57] [MIG] Migrate letsencrypt 2.0.0 to Odoo 11.0 --- letsencrypt/README.rst | 11 +-- letsencrypt/__manifest__.py | 2 +- letsencrypt/data/ir_config_parameter.xml | 3 - .../migrations/11.0.2.0.0/post-migrate.py | 16 +++- letsencrypt/models/__init__.py | 2 +- letsencrypt/models/letsencrypt.py | 6 +- ...fig_settings.py => res_config_settings.py} | 11 +-- letsencrypt/readme/CONTRIBUTORS.rst | 1 + letsencrypt/static/description/index.html | 8 +- letsencrypt/tests/test_letsencrypt.py | 24 +++--- letsencrypt/views/base_config_settings.xml | 33 -------- letsencrypt/views/res_config_settings.xml | 75 +++++++++++++++++++ 12 files changed, 121 insertions(+), 71 deletions(-) rename letsencrypt/models/{base_config_settings.py => res_config_settings.py} (95%) delete mode 100644 letsencrypt/views/base_config_settings.xml create mode 100644 letsencrypt/views/res_config_settings.xml diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst index dba2941dfb4..5ba19fa2d81 100644 --- a/letsencrypt/README.rst +++ b/letsencrypt/README.rst @@ -14,13 +14,13 @@ Let's Encrypt :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github - :target: https://github.com/OCA/server-tools/tree/10.0/letsencrypt + :target: https://github.com/OCA/server-tools/tree/11.0/letsencrypt :alt: OCA/server-tools .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-tools-10-0/server-tools-10-0-letsencrypt + :target: https://translation.odoo-community.org/projects/server-tools-11-0/server-tools-11-0-letsencrypt :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/149/10.0 + :target: https://runbot.odoo-community.org/runbot/149/11.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -148,7 +148,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -160,6 +160,7 @@ Authors * Therp BV * Tecnativa +* Acysos S.L Contributors ~~~~~~~~~~~~ @@ -198,6 +199,6 @@ 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. -This module is part of the `OCA/server-tools `_ project on GitHub. +This module is part of the `OCA/server-tools `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index e3f8f9fdca8..faaedf5900e 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -16,7 +16,7 @@ "data": [ "data/ir_config_parameter.xml", "data/ir_cron.xml", - "views/base_config_settings.xml", + "views/res_config_settings.xml", ], "demo": [ "demo/ir_cron.xml", diff --git a/letsencrypt/data/ir_config_parameter.xml b/letsencrypt/data/ir_config_parameter.xml index 9637cbfd86f..804b1357858 100644 --- a/letsencrypt/data/ir_config_parameter.xml +++ b/letsencrypt/data/ir_config_parameter.xml @@ -16,9 +16,6 @@ forcecreate="True"> letsencrypt.backoff 3 - diff --git a/letsencrypt/migrations/11.0.2.0.0/post-migrate.py b/letsencrypt/migrations/11.0.2.0.0/post-migrate.py index 57a6a9c3aa6..60a66058c26 100644 --- a/letsencrypt/migrations/11.0.2.0.0/post-migrate.py +++ b/letsencrypt/migrations/11.0.2.0.0/post-migrate.py @@ -6,6 +6,10 @@ def migrate_altnames(env): config = env["ir.config_parameter"] existing = config.search([("key", "=like", "letsencrypt.altname.%")]) + if not existing: + # We may be migrating from 10.0.2.0.0, in which case + # letsencrypt.altnames already exists and shouldn't be clobbered. + return new_domains = "\n".join(existing.mapped("value")) config.set_param("letsencrypt.altnames", new_domains) existing.unlink() @@ -18,14 +22,18 @@ def migrate_cron(env): jobs = ( env["ir.cron"] .with_context(active_test=False) - .search([("model", "=", "letsencrypt"), ("function", "=", "cron")]) + .search( + [ + ("ir_actions_server_id.model_id.model", "=", "letsencrypt"), + ("ir_actions_server_id.code", "=", "model.cron()"), + ] + ) ) if not jobs: # ir.cron._try_lock doesn't handle empty recordsets well return - jobs.write( - {"function": "_cron", "interval_type": "days", "interval_number": "1"} - ) + jobs.write({"interval_type": "days", "interval_number": "1"}) + jobs.mapped("ir_actions_server_id").write({"code": "model._cron()"}) def migrate(cr, version): diff --git a/letsencrypt/models/__init__.py b/letsencrypt/models/__init__.py index bb6955a8e93..54285a070df 100644 --- a/letsencrypt/models/__init__.py +++ b/letsencrypt/models/__init__.py @@ -1,4 +1,4 @@ # © 2016 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import letsencrypt -from . import base_config_settings +from . import res_config_settings diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 85acdabc39d..e973f43f9e4 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -10,7 +10,7 @@ import re import subprocess import time -import urlparse +import urllib.parse from datetime import datetime, timedelta @@ -197,7 +197,7 @@ def _should_run(self, cert_file, domains): def _cron(self): ir_config_parameter = self.env['ir.config_parameter'] base_url = ir_config_parameter.get_param('web.base.url', 'localhost') - domain = urlparse.urlparse(base_url).hostname + domain = urllib.parse.urlparse(base_url).hostname cert_file = os.path.join(_get_data_dir(), '%s.crt' % domain) domains = self._cascade_domains([domain] + self._get_altnames()) @@ -403,7 +403,7 @@ def _respond_challenge_http(self, challenge, account_key): Respond to the HTTP challenge by writing the file to serve. """ token = self._base64_encode(challenge.token) - challenge_file = os.path.join(_get_challenge_dir(), token.decode()) + challenge_file = os.path.join(_get_challenge_dir(), token) with open(challenge_file, 'w') as file_: file_.write(challenge.validation(account_key)) diff --git a/letsencrypt/models/base_config_settings.py b/letsencrypt/models/res_config_settings.py similarity index 95% rename from letsencrypt/models/base_config_settings.py rename to letsencrypt/models/res_config_settings.py index 2f89183915f..f35a6f8b96b 100644 --- a/letsencrypt/models/base_config_settings.py +++ b/letsencrypt/models/res_config_settings.py @@ -10,8 +10,8 @@ """ -class BaseConfigSettings(models.TransientModel): - _inherit = 'base.config.settings' +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' letsencrypt_altnames = fields.Text( string="Domain names", @@ -69,7 +69,7 @@ def letsencrypt_check_dns_required(self): @api.model def default_get(self, fields_list): - res = super(BaseConfigSettings, self).default_get(fields_list) + res = super().default_get(fields_list) get_param = self.env['ir.config_parameter'].get_param res.update( { @@ -97,8 +97,9 @@ def default_get(self, fields_list): return res @api.multi - def set_dns_provider(self): - self.ensure_one() + def set_values(self): + super().set_values() + self.letsencrypt_check_dns_required() if self.letsencrypt_dns_provider == 'shell': diff --git a/letsencrypt/readme/CONTRIBUTORS.rst b/letsencrypt/readme/CONTRIBUTORS.rst index 5da59e4d396..1aeb78d8db1 100644 --- a/letsencrypt/readme/CONTRIBUTORS.rst +++ b/letsencrypt/readme/CONTRIBUTORS.rst @@ -2,5 +2,6 @@ * Antonio Espinosa * Dave Lasley * Ronald Portier +* Ignacio Ibeas * George Daramouskas * Jan Verbeek diff --git a/letsencrypt/static/description/index.html b/letsencrypt/static/description/index.html index 7706bfc69a0..f24ebd0d6b4 100644 --- a/letsencrypt/static/description/index.html +++ b/letsencrypt/static/description/index.html @@ -367,7 +367,7 @@

Let’s Encrypt

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

+

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

This module was written to have your Odoo installation request SSL certificates from https://letsencrypt.org automatically.

Table of contents

@@ -490,7 +490,7 @@

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.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -500,6 +500,7 @@

Authors

  • Therp BV
  • Tecnativa
  • +
  • Acysos S.L
@@ -509,6 +510,7 @@

Contributors

  • Antonio Espinosa <antonio.espinosa@tecnativa.com>
  • Dave Lasley <dave@laslabs.com>
  • Ronald Portier <ronald@therp.nl>
  • +
  • Ignacio Ibeas <ignacio@acysos.com>
  • George Daramouskas <gdaramouskas@therp.nl>
  • Jan Verbeek <jverbeek@therp.nl>
  • @@ -535,7 +537,7 @@

    Maintainers

    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.

    -

    This module is part of the OCA/server-tools project on GitHub.

    +

    This module is part of the OCA/server-tools project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index 29226491b5a..f7e693a0212 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -1,8 +1,6 @@ # Copyright 2018 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from __future__ import unicode_literals # cryptography is picky - import os import shutil @@ -28,21 +26,21 @@ def _poll(order, deadline): class TestLetsencrypt(SingleTransactionCase): def setUp(self): - super(TestLetsencrypt, self).setUp() + super().setUp() self.env['ir.config_parameter'].set_param( 'web.base.url', 'http://www.example.com' ) - self.env['base.config.settings'].create( + self.env['res.config.settings'].create( { 'letsencrypt_dns_provider': 'shell', 'letsencrypt_dns_shell_script': 'touch /tmp/.letsencrypt_test', 'letsencrypt_altnames': '*.example.com', 'letsencrypt_reload_command': 'true', # i.e. /bin/true } - ).set_dns_provider() + ).set_values() def test_config_settings(self): - setting_vals = self.env['base.config.settings'].default_get([]) + setting_vals = self.env['res.config.settings'].default_get([]) self.assertEqual(setting_vals['letsencrypt_dns_provider'], 'shell') self.assertEqual( setting_vals['letsencrypt_dns_shell_script'], @@ -57,9 +55,9 @@ def test_config_settings(self): @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=_poll) def test_http_challenge(self, poll, answer_challenge): letsencrypt = self.env['letsencrypt'] - self.env['base.config.settings'].create( + self.env['res.config.settings'].create( {'letsencrypt_altnames': 'test.example.com'} - ).set_dns_provider() + ).set_values() letsencrypt._cron() poll.assert_called() self.assertTrue(os.listdir(_get_challenge_dir())) @@ -98,22 +96,22 @@ def register_update(challenge, domain, token): ) def test_dns_challenge_error_on_missing_provider(self): - self.env['base.config.settings'].create( + self.env['res.config.settings'].create( { 'letsencrypt_altnames': '*.example.com', 'letsencrypt_dns_provider': False, } - ).set_dns_provider() + ).set_values() with self.assertRaises(UserError): self.env['letsencrypt']._cron() def test_prefer_dns_setting(self): - self.env['base.config.settings'].create( + self.env['res.config.settings'].create( { 'letsencrypt_altnames': 'example.com', 'letsencrypt_prefer_dns': True, } - ).set_dns_provider() + ).set_values() self.env['ir.config_parameter'].set_param( 'web.base.url', 'http://example.com' ) @@ -327,7 +325,7 @@ def install_certificate( file_.write(cert.public_bytes(serialization.Encoding.PEM)) def tearDown(self): - super(TestLetsencrypt, self).tearDown() + super().tearDown() shutil.rmtree(_get_data_dir(), ignore_errors=True) if path.isfile('/tmp/.letsencrypt_test'): os.remove('/tmp/.letsencrypt_test') diff --git a/letsencrypt/views/base_config_settings.xml b/letsencrypt/views/base_config_settings.xml deleted file mode 100644 index 93402ee1ebb..00000000000 --- a/letsencrypt/views/base_config_settings.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - Letsencrypt base config settings - base.config.settings - - - - - - - - - - - - - - - - - - diff --git a/letsencrypt/views/res_config_settings.xml b/letsencrypt/views/res_config_settings.xml new file mode 100644 index 00000000000..5bb56b4299c --- /dev/null +++ b/letsencrypt/views/res_config_settings.xml @@ -0,0 +1,75 @@ + + + Letsencrypt settings view + res.config.settings + + + +
    +
    +

    Let's Encrypt

    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    From 67ab94a2cb16951b8638371d62d22c7d3f756125 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Thu, 28 May 2020 11:44:15 +0200 Subject: [PATCH 14/57] [FIX] letsencrypt: Increase test coverage --- letsencrypt/tests/__init__.py | 1 + letsencrypt/tests/test_http.py | 26 +++++++++++++++++ letsencrypt/tests/test_letsencrypt.py | 42 +++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 letsencrypt/tests/test_http.py diff --git a/letsencrypt/tests/__init__.py b/letsencrypt/tests/__init__.py index 4a7abea1fc7..f5551bae996 100644 --- a/letsencrypt/tests/__init__.py +++ b/letsencrypt/tests/__init__.py @@ -1,3 +1,4 @@ # Copyright 2018 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_http from . import test_letsencrypt diff --git a/letsencrypt/tests/test_http.py b/letsencrypt/tests/test_http.py new file mode 100644 index 00000000000..d28032b46ce --- /dev/null +++ b/letsencrypt/tests/test_http.py @@ -0,0 +1,26 @@ +# Copyright 2020 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import os +import shutil + +from odoo.tests import HttpCase + +from ..models.letsencrypt import _get_challenge_dir + + +class TestHTTP(HttpCase): + def test_query_existing(self): + with open(os.path.join(_get_challenge_dir(), "foobar"), "w") as file: + file.write("content") + res = self.url_open("/.well-known/acme-challenge/foobar") + self.assertEqual(res.status_code, 200) + self.assertEqual(res.text, "content") + + def test_query_missing(self): + res = self.url_open("/.well-known/acme-challenge/foobar") + self.assertEqual(res.status_code, 404) + + def tearDown(self): + super().tearDown() + shutil.rmtree(_get_challenge_dir(), ignore_errors=True) diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index f7e693a0212..cd9621b02de 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -9,9 +9,14 @@ import mock -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError from odoo.tests import SingleTransactionCase +try: + import dns.resolver +except ImportError: + pass + from ..models.letsencrypt import _get_data_dir, _get_challenge_dir @@ -35,7 +40,7 @@ def setUp(self): 'letsencrypt_dns_provider': 'shell', 'letsencrypt_dns_shell_script': 'touch /tmp/.letsencrypt_test', 'letsencrypt_altnames': '*.example.com', - 'letsencrypt_reload_command': 'true', # i.e. /bin/true + 'letsencrypt_reload_command': 'echo reloaded', } ).set_values() @@ -47,13 +52,18 @@ def test_config_settings(self): 'touch /tmp/.letsencrypt_test', ) self.assertEqual(setting_vals['letsencrypt_altnames'], '*.example.com') - self.assertEqual(setting_vals['letsencrypt_reload_command'], 'true') + self.assertEqual(setting_vals['letsencrypt_reload_command'], 'echo reloaded') self.assertTrue(setting_vals['letsencrypt_needs_dns_provider']) self.assertFalse(setting_vals['letsencrypt_prefer_dns']) + with self.assertRaises(ValidationError): + self.env["res.config.settings"].create( + {"letsencrypt_dns_shell_script": "# Empty script"} + ).set_values() + @mock.patch('acme.client.ClientV2.answer_challenge') @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=_poll) - def test_http_challenge(self, poll, answer_challenge): + def test_http_challenge(self, poll, _answer_challenge): letsencrypt = self.env['letsencrypt'] self.env['res.config.settings'].create( {'letsencrypt_altnames': 'test.example.com'} @@ -74,10 +84,12 @@ def test_http_challenge(self, poll, answer_challenge): @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=_poll) def test_dns_challenge(self, poll, answer_challenge, sleep, query, dnsupd): + record = None + def register_update(challenge, domain, token): + nonlocal record record = mock.Mock() record.to_text.return_value = '"%s"' % token - query.return_value = [record] ret = mock.Mock() ret.challenge = challenge ret.domain = domain @@ -86,10 +98,28 @@ def register_update(challenge, domain, token): dnsupd.side_effect = register_update + ncalls = 0 + + def query_effect(domain, rectype): + nonlocal ncalls + self.assertEqual(domain, "_acme-challenge.example.com.") + self.assertEqual(rectype, "TXT") + ncalls += 1 + if ncalls == 1: + raise dns.resolver.NXDOMAIN + elif ncalls == 2: + wrong_record = mock.Mock() + wrong_record.to_text.return_value = '"not right"' + return [wrong_record] + else: + return [record] + + query.side_effect = query_effect + self.install_certificate(days_left=10) self.env['letsencrypt']._cron() poll.assert_called() - query.assert_called_with("_acme-challenge.example.com.", "TXT") + self.assertEqual(ncalls, 3) self.assertTrue(path.isfile('/tmp/.letsencrypt_test')) self.assertTrue( path.isfile(path.join(_get_data_dir(), 'www.example.com.crt')) From 3be4114a7b47446ae73954bece1b147226f3ab0a Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Thu, 28 May 2020 14:59:00 +0200 Subject: [PATCH 15/57] [MIG] Migrate letsencrypt 2.0.0 to Odoo 12.0 --- letsencrypt/README.rst | 10 +-- letsencrypt/__manifest__.py | 2 +- letsencrypt/data/ir_config_parameter.xml | 32 ++++---- letsencrypt/data/ir_cron.xml | 24 +++--- .../post-migrate.py | 3 +- letsencrypt/models/res_config_settings.py | 81 +++++++++---------- letsencrypt/static/description/index.html | 6 +- 7 files changed, 72 insertions(+), 86 deletions(-) rename letsencrypt/migrations/{11.0.2.0.0 => 12.0.2.0.0}/post-migrate.py (90%) diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst index 5ba19fa2d81..7ea09352030 100644 --- a/letsencrypt/README.rst +++ b/letsencrypt/README.rst @@ -14,13 +14,13 @@ Let's Encrypt :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github - :target: https://github.com/OCA/server-tools/tree/11.0/letsencrypt + :target: https://github.com/OCA/server-tools/tree/12.0/letsencrypt :alt: OCA/server-tools .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-tools-11-0/server-tools-11-0-letsencrypt + :target: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-letsencrypt :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/149/11.0 + :target: https://runbot.odoo-community.org/runbot/149/12.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -148,7 +148,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -199,6 +199,6 @@ 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. -This module is part of the `OCA/server-tools `_ project on GitHub. +This module is part of the `OCA/server-tools `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index faaedf5900e..5172113ee6f 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { "name": "Let's Encrypt", - "version": "11.0.2.0.0", + "version": "12.0.2.0.0", "author": "Therp BV," "Tecnativa," "Acysos S.L," diff --git a/letsencrypt/data/ir_config_parameter.xml b/letsencrypt/data/ir_config_parameter.xml index 804b1357858..737ca4bec40 100644 --- a/letsencrypt/data/ir_config_parameter.xml +++ b/letsencrypt/data/ir_config_parameter.xml @@ -1,22 +1,20 @@ - - + - - letsencrypt.reload_command - sudo /usr/sbin/service nginx reload - + + letsencrypt.reload_command + sudo /usr/sbin/service nginx reload + - - letsencrypt.backoff - 3 - + + letsencrypt.backoff + 3 + - diff --git a/letsencrypt/data/ir_cron.xml b/letsencrypt/data/ir_cron.xml index e707ba15cc9..cd8232c5d20 100644 --- a/letsencrypt/data/ir_cron.xml +++ b/letsencrypt/data/ir_cron.xml @@ -1,14 +1,12 @@ - - - - Check Let's Encrypt certificates - - code - model._cron() - days - 1 - -1 - - - + + + Check Let's Encrypt certificates + + code + model._cron() + days + 1 + -1 + + diff --git a/letsencrypt/migrations/11.0.2.0.0/post-migrate.py b/letsencrypt/migrations/12.0.2.0.0/post-migrate.py similarity index 90% rename from letsencrypt/migrations/11.0.2.0.0/post-migrate.py rename to letsencrypt/migrations/12.0.2.0.0/post-migrate.py index 60a66058c26..0da93ce11ce 100644 --- a/letsencrypt/migrations/11.0.2.0.0/post-migrate.py +++ b/letsencrypt/migrations/12.0.2.0.0/post-migrate.py @@ -7,8 +7,7 @@ def migrate_altnames(env): config = env["ir.config_parameter"] existing = config.search([("key", "=like", "letsencrypt.altname.%")]) if not existing: - # We may be migrating from 10.0.2.0.0, in which case - # letsencrypt.altnames already exists and shouldn't be clobbered. + # If letsencrypt.altnames already exists it shouldn't be clobbered return new_domains = "\n".join(existing.mapped("value")) config.set_param("letsencrypt.altnames", new_domains) diff --git a/letsencrypt/models/res_config_settings.py b/letsencrypt/models/res_config_settings.py index f35a6f8b96b..dc281e8ecd8 100644 --- a/letsencrypt/models/res_config_settings.py +++ b/letsencrypt/models/res_config_settings.py @@ -1,7 +1,7 @@ # Copyright 2018 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, exceptions, fields, models +from odoo import _, api, exceptions, fields, models DNS_SCRIPT_DEFAULT = """# Write your script here @@ -20,6 +20,7 @@ class ResConfigSettings(models.TransientModel): 'Additional domains to include on the CSR. ' 'Separate with commas or newlines.' ), + force_config_parameter="letsencrypt.altnames", ) letsencrypt_dns_provider = fields.Selection( selection=[('shell', 'Shell script')], @@ -30,6 +31,7 @@ class ResConfigSettings(models.TransientModel): 'script. Other options can be added by installing additional ' 'modules.' ), + config_parameter="letsencrypt.dns_provider", ) letsencrypt_dns_shell_script = fields.Text( string='DNS update script', @@ -39,11 +41,13 @@ class ResConfigSettings(models.TransientModel): '$LETSENCRYPT_DNS_DOMAIN variables.' ), default=DNS_SCRIPT_DEFAULT, + force_config_parameter="letsencrypt.dns_shell_script", ) letsencrypt_needs_dns_provider = fields.Boolean() letsencrypt_reload_command = fields.Text( string='Server reload command', help='Fill this with the command to restart your web server.', + force_config_parameter="letsencrypt.reload_command", ) letsencrypt_testing_mode = fields.Boolean( string='Use testing server', @@ -51,13 +55,15 @@ class ResConfigSettings(models.TransientModel): "Use the Let's Encrypt staging server, which has higher rate " "limits but doesn't create valid certificates." ), + config_parameter="letsencrypt.testing_mode", ) letsencrypt_prefer_dns = fields.Boolean( string="Prefer DNS validation", help=( "Validate through DNS even when HTTP validation is possible. " - "Use this if your Odoo instance isn't publicly accessible.", - ) + "Use this if your Odoo instance isn't publicly accessible." + ), + config_parameter="letsencrypt.prefer_dns", ) @api.onchange('letsencrypt_altnames', 'letsencrypt_prefer_dns') @@ -70,29 +76,9 @@ def letsencrypt_check_dns_required(self): @api.model def default_get(self, fields_list): res = super().default_get(fields_list) - get_param = self.env['ir.config_parameter'].get_param - res.update( - { - 'letsencrypt_dns_provider': get_param( - 'letsencrypt.dns_provider' - ), - 'letsencrypt_dns_shell_script': get_param( - 'letsencrypt.dns_shell_script', DNS_SCRIPT_DEFAULT - ), - 'letsencrypt_altnames': get_param('letsencrypt.altnames', ''), - 'letsencrypt_reload_command': get_param( - 'letsencrypt.reload_command' - ), - 'letsencrypt_needs_dns_provider': ( - '*.' in get_param('letsencrypt.altnames', '') - ), - 'letsencrypt_testing_mode': ( - get_param('letsencrypt.testing_mode', 'False') == 'True' - ), - 'letsencrypt_prefer_dns': ( - get_param('letsencrypt.prefer_dns', 'False') == 'True' - ), - } + res["letsencrypt_needs_dns_provider"] = ( + "*." in res["letsencrypt_altnames"] + or res["letsencrypt_prefer_dns"] ) return res @@ -109,24 +95,29 @@ def set_values(self): ] if all(line == '' or line.startswith('#') for line in lines): raise exceptions.ValidationError( - "You didn't write a DNS update script!" + _("You didn't write a DNS update script!") ) - set_param = self.env['ir.config_parameter'].set_param - set_param('letsencrypt.dns_provider', self.letsencrypt_dns_provider) - set_param( - 'letsencrypt.dns_shell_script', self.letsencrypt_dns_shell_script - ) - set_param('letsencrypt.altnames', self.letsencrypt_altnames) - set_param( - 'letsencrypt.reload_command', self.letsencrypt_reload_command - ) - set_param( - 'letsencrypt.testing_mode', - 'True' if self.letsencrypt_testing_mode else 'False', - ) - set_param( - 'letsencrypt.prefer_dns', - 'True' if self.letsencrypt_prefer_dns else 'False', - ) - return True + @api.model + def _get_classified_fields(self): + """config_parameter is inexplicably allowed on Char but not Text. + + ir.config_parameter values can handle newlines, even in the web + interface, so there doesn't seem to be a good reason for this. + + Add "force_config_parameter" as an alternative that ignores types. + """ + # Note: As of writing, Odoo 13 has this limit and Odoo 14 isn't out + # Whenever migrating, make sure this is still necessary + classified = super()._get_classified_fields() + new_other = [] + for name in classified["other"]: + field = self._fields[name] + if hasattr(field, "force_config_parameter"): + classified["config"].append( + (name, field.force_config_parameter) + ) + else: + new_other.append(name) + classified["other"] = new_other + return classified diff --git a/letsencrypt/static/description/index.html b/letsencrypt/static/description/index.html index f24ebd0d6b4..c59f495c2f6 100644 --- a/letsencrypt/static/description/index.html +++ b/letsencrypt/static/description/index.html @@ -367,7 +367,7 @@

    Let’s Encrypt

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

    Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

    +

    Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

    This module was written to have your Odoo installation request SSL certificates from https://letsencrypt.org automatically.

    Table of contents

    @@ -490,7 +490,7 @@

    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.

    +feedback.

    Do not contact contributors directly about support or help with technical issues.

    @@ -537,7 +537,7 @@

    Maintainers

    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.

    -

    This module is part of the OCA/server-tools project on GitHub.

    +

    This module is part of the OCA/server-tools project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    From 214054d1b5001c3beaabd07f9b03ff4d5c7c4e37 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Tue, 11 Aug 2020 15:58:52 +0200 Subject: [PATCH 16/57] [UPD] letsencrypt: Update hbrunn's email address --- letsencrypt/README.rst | 2 +- letsencrypt/readme/CONTRIBUTORS.rst | 2 +- letsencrypt/static/description/index.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst index 7ea09352030..3a04e6f2cee 100644 --- a/letsencrypt/README.rst +++ b/letsencrypt/README.rst @@ -165,7 +165,7 @@ Authors Contributors ~~~~~~~~~~~~ -* Holger Brunn +* Holger Brunn * Antonio Espinosa * Dave Lasley * Ronald Portier diff --git a/letsencrypt/readme/CONTRIBUTORS.rst b/letsencrypt/readme/CONTRIBUTORS.rst index 1aeb78d8db1..46b22c02abe 100644 --- a/letsencrypt/readme/CONTRIBUTORS.rst +++ b/letsencrypt/readme/CONTRIBUTORS.rst @@ -1,4 +1,4 @@ -* Holger Brunn +* Holger Brunn * Antonio Espinosa * Dave Lasley * Ronald Portier diff --git a/letsencrypt/static/description/index.html b/letsencrypt/static/description/index.html index c59f495c2f6..79d84c26fad 100644 --- a/letsencrypt/static/description/index.html +++ b/letsencrypt/static/description/index.html @@ -506,7 +506,7 @@

    Authors

    Contributors

      -
    • Holger Brunn <hbrunn@therp.nl>
    • +
    • Holger Brunn <mail@hunki-enterprises.nl>
    • Antonio Espinosa <antonio.espinosa@tecnativa.com>
    • Dave Lasley <dave@laslabs.com>
    • Ronald Portier <ronald@therp.nl>
    • From 9d5d0b6af83bb50cf72cfb0dddbc5d4ad5cb32d6 Mon Sep 17 00:00:00 2001 From: Jan Verbeek Date: Tue, 11 Aug 2020 17:05:38 +0200 Subject: [PATCH 17/57] [IMP] letsencrypt: Handle web.base.url in a saner way --- .../migrations/12.0.2.0.0/post-migrate.py | 15 ++++++++-- letsencrypt/models/letsencrypt.py | 14 ++++----- letsencrypt/models/res_config_settings.py | 2 +- letsencrypt/tests/test_letsencrypt.py | 29 ++++++++++++------- letsencrypt/views/res_config_settings.xml | 2 +- 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/letsencrypt/migrations/12.0.2.0.0/post-migrate.py b/letsencrypt/migrations/12.0.2.0.0/post-migrate.py index 0da93ce11ce..14ae8e2eb9d 100644 --- a/letsencrypt/migrations/12.0.2.0.0/post-migrate.py +++ b/letsencrypt/migrations/12.0.2.0.0/post-migrate.py @@ -1,5 +1,7 @@ # Copyright 2018 Therp BV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import urllib.parse + from odoo import api, SUPERUSER_ID @@ -9,8 +11,17 @@ def migrate_altnames(env): if not existing: # If letsencrypt.altnames already exists it shouldn't be clobbered return - new_domains = "\n".join(existing.mapped("value")) - config.set_param("letsencrypt.altnames", new_domains) + domains = existing.mapped("value") + base_url = config.get_param("web.base.url", "http://localhost:8069") + base_domain = urllib.parse.urlparse(base_url).hostname + if ( + domains + and base_domain + and base_domain != "localhost" + and base_domain not in domains + ): + domains.insert(0, base_domain) + config.set_param("letsencrypt.altnames", "\n".join(domains)) existing.unlink() diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index e973f43f9e4..305204310f0 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -196,11 +196,11 @@ def _should_run(self, cert_file, domains): @api.model def _cron(self): ir_config_parameter = self.env['ir.config_parameter'] - base_url = ir_config_parameter.get_param('web.base.url', 'localhost') - domain = urllib.parse.urlparse(base_url).hostname + domains = self._get_altnames() + domain = domains[0] cert_file = os.path.join(_get_data_dir(), '%s.crt' % domain) - domains = self._cascade_domains([domain] + self._get_altnames()) + domains = self._cascade_domains(domains) for dom in domains: self._validate_domain(dom) @@ -390,11 +390,11 @@ def _cascade_domains(self, domains): @api.model def _get_altnames(self): """Get the configured altnames as a list of strings.""" - altnames = self.env['ir.config_parameter'].get_param( - 'letsencrypt.altnames' - ) + parameter = self.env['ir.config_parameter'] + altnames = parameter.get_param("letsencrypt.altnames") if not altnames: - return [] + base_url = parameter.get_param("web.base.url", "http://localhost") + return [urllib.parse.urlparse(base_url).hostname] return re.split('(?:,|\n| |;)+', altnames) @api.model diff --git a/letsencrypt/models/res_config_settings.py b/letsencrypt/models/res_config_settings.py index dc281e8ecd8..530ea81aa25 100644 --- a/letsencrypt/models/res_config_settings.py +++ b/letsencrypt/models/res_config_settings.py @@ -17,7 +17,7 @@ class ResConfigSettings(models.TransientModel): string="Domain names", default='', help=( - 'Additional domains to include on the CSR. ' + 'Domains to use for the certificate. ' 'Separate with commas or newlines.' ), force_config_parameter="letsencrypt.altnames", diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index cd9621b02de..4e43c1f711c 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -39,7 +39,7 @@ def setUp(self): { 'letsencrypt_dns_provider': 'shell', 'letsencrypt_dns_shell_script': 'touch /tmp/.letsencrypt_test', - 'letsencrypt_altnames': '*.example.com', + 'letsencrypt_altnames': 'www.example.com,*.example.com', 'letsencrypt_reload_command': 'echo reloaded', } ).set_values() @@ -51,7 +51,10 @@ def test_config_settings(self): setting_vals['letsencrypt_dns_shell_script'], 'touch /tmp/.letsencrypt_test', ) - self.assertEqual(setting_vals['letsencrypt_altnames'], '*.example.com') + self.assertEqual( + setting_vals['letsencrypt_altnames'], + 'www.example.com,*.example.com' + ) self.assertEqual(setting_vals['letsencrypt_reload_command'], 'echo reloaded') self.assertTrue(setting_vals['letsencrypt_needs_dns_provider']) self.assertFalse(setting_vals['letsencrypt_prefer_dns']) @@ -66,7 +69,7 @@ def test_config_settings(self): def test_http_challenge(self, poll, _answer_challenge): letsencrypt = self.env['letsencrypt'] self.env['res.config.settings'].create( - {'letsencrypt_altnames': 'test.example.com'} + {'letsencrypt_altnames': ''} ).set_values() letsencrypt._cron() poll.assert_called() @@ -142,9 +145,6 @@ def test_prefer_dns_setting(self): 'letsencrypt_prefer_dns': True, } ).set_values() - self.env['ir.config_parameter'].set_param( - 'web.base.url', 'http://example.com' - ) # pylint: disable=no-value-for-parameter self.test_dns_challenge() @@ -189,14 +189,17 @@ def test_altnames_parsing(self): config = self.env['ir.config_parameter'] letsencrypt = self.env['letsencrypt'] - self.assertEqual(letsencrypt._get_altnames(), ['*.example.com']) + self.assertEqual( + letsencrypt._get_altnames(), + ['www.example.com', '*.example.com'] + ) config.set_param('letsencrypt.altnames', '') - self.assertEqual(letsencrypt._get_altnames(), []) - - config.set_param('letsencrypt.altnames', 'www.example.com') self.assertEqual(letsencrypt._get_altnames(), ['www.example.com']) + config.set_param('letsencrypt.altnames', 'foobar.example.com') + self.assertEqual(letsencrypt._get_altnames(), ['foobar.example.com']) + config.set_param( 'letsencrypt.altnames', 'example.com,example.org,example.net' ) @@ -294,6 +297,12 @@ def test_new_altnames(self): ['www.example.com', '*.example.com'], ) ) + self.assertFalse( + self.env['letsencrypt']._should_run( + path.join(_get_data_dir(), 'www.example.com.crt'), + ['www.example.com'], + ) + ) def test_legacy_certificate_without_altnames(self): self.install_certificate(60, use_altnames=False) diff --git a/letsencrypt/views/res_config_settings.xml b/letsencrypt/views/res_config_settings.xml index 5bb56b4299c..eacc48d8d76 100644 --- a/letsencrypt/views/res_config_settings.xml +++ b/letsencrypt/views/res_config_settings.xml @@ -20,7 +20,7 @@
      From 665fc46c29abbcb8bfd9701642a293e32160aa56 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 19 Nov 2020 15:32:40 +0100 Subject: [PATCH 18/57] [IMP] letsencrypt: black, isort, prettier --- letsencrypt/__manifest__.py | 28 +- letsencrypt/controllers/main.py | 4 +- letsencrypt/data/ir_config_parameter.xml | 15 +- letsencrypt/data/ir_cron.xml | 4 +- letsencrypt/demo/ir_cron.xml | 2 +- letsencrypt/hooks.py | 2 +- .../migrations/12.0.2.0.0/post-migrate.py | 52 ---- letsencrypt/models/letsencrypt.py | 157 +++++------ letsencrypt/models/res_config_settings.py | 50 ++-- letsencrypt/tests/test_letsencrypt.py | 250 ++++++++---------- letsencrypt/views/res_config_settings.xml | 71 +++-- 11 files changed, 260 insertions(+), 375 deletions(-) delete mode 100644 letsencrypt/migrations/12.0.2.0.0/post-migrate.py diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index 5172113ee6f..8053da73145 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -1,34 +1,20 @@ -# © 2016 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# 2016-2020 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Let's Encrypt", - "version": "12.0.2.0.0", - "author": "Therp BV," - "Tecnativa," - "Acysos S.L," - "Odoo Community Association (OCA)", + "version": "13.0.1.0.0", + "author": "Therp BV," "Tecnativa," "Acysos S.L," "Odoo Community Association (OCA)", "license": "AGPL-3", "category": "Hidden/Dependency", "summary": "Request SSL certificates from letsencrypt.org", - "depends": [ - "base_setup", - ], + "depends": ["base_setup"], "data": [ "data/ir_config_parameter.xml", "data/ir_cron.xml", "views/res_config_settings.xml", ], - "demo": [ - "demo/ir_cron.xml", - ], + "demo": ["demo/ir_cron.xml"], "post_init_hook": "post_init_hook", "installable": True, - "external_dependencies": { - "python": [ - "acme", - "cryptography", - "dns", - "josepy", - ], - }, + "external_dependencies": {"python": ["acme", "cryptography", "dns", "josepy"]}, } diff --git a/letsencrypt/controllers/main.py b/letsencrypt/controllers/main.py index bba661710ce..2dfbc01a17f 100644 --- a/letsencrypt/controllers/main.py +++ b/letsencrypt/controllers/main.py @@ -3,13 +3,15 @@ # © 2018 Ignacio Ibeas # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import os + from odoo import http from odoo.http import request + from ..models.letsencrypt import _get_challenge_dir class Letsencrypt(http.Controller): - @http.route('/.well-known/acme-challenge/', auth='none') + @http.route("/.well-known/acme-challenge/", auth="none") def acme_challenge(self, filename): try: with open(os.path.join(_get_challenge_dir(), filename)) as key: diff --git a/letsencrypt/data/ir_config_parameter.xml b/letsencrypt/data/ir_config_parameter.xml index 737ca4bec40..9e7c28881a7 100644 --- a/letsencrypt/data/ir_config_parameter.xml +++ b/letsencrypt/data/ir_config_parameter.xml @@ -1,20 +1,11 @@ - + - - + letsencrypt.reload_command sudo /usr/sbin/service nginx reload - - + letsencrypt.backoff 3 - diff --git a/letsencrypt/data/ir_cron.xml b/letsencrypt/data/ir_cron.xml index cd8232c5d20..64c2fc7ac4b 100644 --- a/letsencrypt/data/ir_cron.xml +++ b/letsencrypt/data/ir_cron.xml @@ -1,8 +1,8 @@ - + Check Let's Encrypt certificates - + code model._cron() days diff --git a/letsencrypt/demo/ir_cron.xml b/letsencrypt/demo/ir_cron.xml index 926e80d367a..0228a5b7254 100644 --- a/letsencrypt/demo/ir_cron.xml +++ b/letsencrypt/demo/ir_cron.xml @@ -1,4 +1,4 @@ - + diff --git a/letsencrypt/hooks.py b/letsencrypt/hooks.py index 087deaf779d..a236a41e858 100644 --- a/letsencrypt/hooks.py +++ b/letsencrypt/hooks.py @@ -5,4 +5,4 @@ def post_init_hook(cr, pool): env = api.Environment(cr, SUPERUSER_ID, {}) - env['letsencrypt']._get_key('account.key') + env["letsencrypt"]._get_key("account.key") diff --git a/letsencrypt/migrations/12.0.2.0.0/post-migrate.py b/letsencrypt/migrations/12.0.2.0.0/post-migrate.py deleted file mode 100644 index 14ae8e2eb9d..00000000000 --- a/letsencrypt/migrations/12.0.2.0.0/post-migrate.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2018 Therp BV -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -import urllib.parse - -from odoo import api, SUPERUSER_ID - - -def migrate_altnames(env): - config = env["ir.config_parameter"] - existing = config.search([("key", "=like", "letsencrypt.altname.%")]) - if not existing: - # If letsencrypt.altnames already exists it shouldn't be clobbered - return - domains = existing.mapped("value") - base_url = config.get_param("web.base.url", "http://localhost:8069") - base_domain = urllib.parse.urlparse(base_url).hostname - if ( - domains - and base_domain - and base_domain != "localhost" - and base_domain not in domains - ): - domains.insert(0, base_domain) - config.set_param("letsencrypt.altnames", "\n".join(domains)) - existing.unlink() - - -def migrate_cron(env): - # Any interval that was appropriate for the old version is inappropriate - # for the new one, so it's ok to clobber it. - # But tweaking it afterwards is fine, so noupdate="1" still makes sense. - jobs = ( - env["ir.cron"] - .with_context(active_test=False) - .search( - [ - ("ir_actions_server_id.model_id.model", "=", "letsencrypt"), - ("ir_actions_server_id.code", "=", "model.cron()"), - ] - ) - ) - if not jobs: - # ir.cron._try_lock doesn't handle empty recordsets well - return - jobs.write({"interval_type": "days", "interval_number": "1"}) - jobs.mapped("ir_actions_server_id").write({"code": "model._cron()"}) - - -def migrate(cr, version): - env = api.Environment(cr, SUPERUSER_ID, {}) - migrate_altnames(env) - migrate_cron(env) diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 305204310f0..2df4c9bef7b 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -1,7 +1,7 @@ -# © 2016 Therp BV -# © 2016 Antonio Espinosa -# © 2018 Ignacio Ibeas -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2016-2020 Therp BV . +# Copyright 2016 Antonio Espinosa . +# Copyright 2018 Ignacio Ibeas . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import base64 import collections @@ -11,7 +11,6 @@ import subprocess import time import urllib.parse - from datetime import datetime, timedelta import requests @@ -39,45 +38,41 @@ except ImportError as e: _logger.debug(e) -WILDCARD = '*.' # as defined in the spec +WILDCARD = "*." # as defined in the spec DEFAULT_KEY_LENGTH = 4096 -TYPE_CHALLENGE_HTTP = 'http-01' -TYPE_CHALLENGE_DNS = 'dns-01' -V2_STAGING_DIRECTORY_URL = ( - 'https://acme-staging-v02.api.letsencrypt.org/directory' -) -V2_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory' +TYPE_CHALLENGE_HTTP = "http-01" +TYPE_CHALLENGE_DNS = "dns-01" +V2_STAGING_DIRECTORY_URL = "https://acme-staging-v02.api.letsencrypt.org/directory" +V2_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory" LOCAL_DOMAINS = { - 'localhost', - 'localhost.localdomain', - 'localhost6', - 'localhost6.localdomain6', - 'ip6-localhost', - 'ip6-loopback', + "localhost", + "localhost.localdomain", + "localhost6", + "localhost6.localdomain6", + "ip6-localhost", + "ip6-loopback", } -DNSUpdate = collections.namedtuple( - "DNSUpdate", ("challenge", "domain", "token") -) +DNSUpdate = collections.namedtuple("DNSUpdate", ("challenge", "domain", "token")) def _get_data_dir(): - dir_ = os.path.join(config.options.get('data_dir'), 'letsencrypt') + dir_ = os.path.join(config.options.get("data_dir"), "letsencrypt") if not os.path.isdir(dir_): os.makedirs(dir_) return dir_ def _get_challenge_dir(): - dir_ = os.path.join(_get_data_dir(), 'acme-challenge') + dir_ = os.path.join(_get_data_dir(), "acme-challenge") if not os.path.isdir(dir_): os.makedirs(dir_) return dir_ class Letsencrypt(models.AbstractModel): - _name = 'letsencrypt' - _description = 'Abstract model providing functions for letsencrypt' + _name = "letsencrypt" + _description = "Abstract model providing functions for letsencrypt" @api.model def _generate_key(self): @@ -100,7 +95,7 @@ def _get_key(self, key_name): _logger.info("Generating new key %s", key_name) key_bytes = self._generate_key() try: - with open(key_file, 'wb') as file_: + with open(key_file, "wb") as file_: os.fchmod(file_.fileno(), 0o600) file_.write(key_bytes) except BaseException: @@ -110,25 +105,21 @@ def _get_key(self, key_name): raise else: _logger.info("Getting existing key %s", key_name) - with open(key_file, 'rb') as file_: + with open(key_file, "rb") as file_: key_bytes = file_.read() return key_bytes @api.model def _validate_domain(self, domain): """Validate that a domain is publicly accessible.""" - if ':' in domain or all( - char.isdigit() or char == '.' for char in domain - ): + if ":" in domain or all(char.isdigit() or char == "." for char in domain): raise UserError( - _("Domain %s: Let's Encrypt doesn't support IP addresses!") - % domain + _("Domain %s: Let's Encrypt doesn't support IP addresses!") % domain ) - if domain in LOCAL_DOMAINS or '.' not in domain: + if domain in LOCAL_DOMAINS or "." not in domain: raise UserError( - _("Domain %s: Let's encrypt doesn't work with local domains!") - % domain + _("Domain %s: Let's encrypt doesn't work with local domains!") % domain ) @api.model @@ -140,10 +131,8 @@ def _should_run(self, cert_file, domains): _logger.info("No existing certificate found, creating a new one") return True - with open(cert_file, 'rb') as file_: - cert = x509.load_pem_x509_certificate( - file_.read(), default_backend() - ) + with open(cert_file, "rb") as file_: + cert = x509.load_pem_x509_certificate(file_.read(), default_backend()) expiry = cert.not_valid_after remaining = expiry - datetime.now() if remaining < timedelta(): @@ -181,8 +170,7 @@ def _should_run(self, cert_file, domains): missing = domains - names if missing: _logger.info( - "Found new domains %s, requesting new certificate", - ', '.join(missing), + "Found new domains %s, requesting new certificate", ", ".join(missing), ) return True @@ -195,10 +183,10 @@ def _should_run(self, cert_file, domains): @api.model def _cron(self): - ir_config_parameter = self.env['ir.config_parameter'] + ir_config_parameter = self.env["ir.config_parameter"] domains = self._get_altnames() domain = domains[0] - cert_file = os.path.join(_get_data_dir(), '%s.crt' % domain) + cert_file = os.path.join(_get_data_dir(), "%s.crt" % domain) domains = self._cascade_domains(domains) for dom in domains: @@ -207,8 +195,8 @@ def _cron(self): if not self._should_run(cert_file, domains): return - account_key = josepy.JWKRSA.load(self._get_key('account.key')) - domain_key = self._get_key('%s.key' % domain) + account_key = josepy.JWKRSA.load(self._get_key("account.key")) + domain_key = self._get_key("%s.key" % domain) client = self._create_client(account_key) new_reg = acme.messages.NewRegistration( @@ -219,16 +207,12 @@ def _cron(self): _logger.info("Successfully registered.") except acme.errors.ConflictError as err: reg = acme.messages.Registration(key=account_key.public_key()) - reg_res = acme.messages.RegistrationResource( - body=reg, uri=err.location - ) + reg_res = acme.messages.RegistrationResource(body=reg, uri=err.location) client.query_registration(reg_res) _logger.info("Reusing existing account.") - _logger.info('Making CSR for the following domains: %s', domains) - csr = acme.crypto_util.make_csr( - private_key_pem=domain_key, domains=domains - ) + _logger.info("Making CSR for the following domains: %s", domains) + csr = acme.crypto_util.make_csr(private_key_pem=domain_key, domains=domains) authzr = client.new_order(csr) # For each requested domain name we receive a list of challenges. @@ -260,9 +244,7 @@ def _cron(self): for challenge in ordered_challenges: if challenge.chall.typ == TYPE_CHALLENGE_HTTP: self._respond_challenge_http(challenge, account_key) - client.answer_challenge( - challenge, acme.challenges.HTTP01Response() - ) + client.answer_challenge(challenge, acme.challenges.HTTP01Response()) break elif challenge.chall.typ == TYPE_CHALLENGE_DNS: domain = authorizations.body.identifier.value @@ -271,32 +253,24 @@ def _cron(self): # We delay this because we wait for each domain. # That takes less time if they've all already been changed. pending_responses.append( - DNSUpdate( - challenge=challenge, domain=domain, token=token - ) + DNSUpdate(challenge=challenge, domain=domain, token=token) ) break else: - raise UserError( - _('Could not respond to letsencrypt challenges.') - ) + raise UserError(_("Could not respond to letsencrypt challenges.")) if pending_responses: for update in pending_responses: self._wait_for_record(update.domain, update.token) # 1 minute was not always enough during testing, even once records # were visible locally - _logger.info( - "All TXT records found, waiting 5 minutes more to make sure." - ) + _logger.info("All TXT records found, waiting 5 minutes more to make sure.") time.sleep(300) for update in pending_responses: - client.answer_challenge( - update.challenge, acme.challenges.DNSResponse() - ) + client.answer_challenge(update.challenge, acme.challenges.DNSResponse()) # let them know we are done and they should check - backoff = int(ir_config_parameter.get_param('letsencrypt.backoff', 3)) + backoff = int(ir_config_parameter.get_param("letsencrypt.backoff", 3)) deadline = datetime.now() + timedelta(minutes=backoff) try: order_resource = client.poll_and_finalize(authzr, deadline) @@ -307,12 +281,10 @@ def _cron(self): _logger.error(str(challenge.error)) raise - with open(cert_file, 'w') as crt: + with open(cert_file, "w") as crt: crt.write(order_resource.fullchain_pem) - _logger.info('SUCCESS: Certificate saved: %s', cert_file) - reload_cmd = ir_config_parameter.get_param( - 'letsencrypt.reload_command', '' - ) + _logger.info("SUCCESS: Certificate saved: %s", cert_file) + reload_cmd = ir_config_parameter.get_param("letsencrypt.reload_command", "") if reload_cmd.strip(): self._call_cmdline(reload_cmd) else: @@ -328,9 +300,7 @@ def _wait_for_record(self, domain, token): while True: attempt += 1 try: - for record in dns.resolver.query( - "_acme-challenge." + domain, "TXT" - ): + for record in dns.resolver.query("_acme-challenge." + domain, "TXT"): value = record.to_text()[1:-1] if value == token: return @@ -350,9 +320,9 @@ def _wait_for_record(self, domain, token): @api.model def _create_client(self, account_key): - param = self.env['ir.config_parameter'] - testing_mode = param.get_param('letsencrypt.testing_mode') == 'True' - if config['test_enable'] or testing_mode: + param = self.env["ir.config_parameter"] + testing_mode = param.get_param("letsencrypt.testing_mode") == "True" + if config["test_enable"] or testing_mode: directory_url = V2_STAGING_DIRECTORY_URL else: directory_url = V2_DIRECTORY_URL @@ -382,7 +352,7 @@ def _cascade_domains(self, domains): continue if other.endswith(postfix): prefix = other[: -len(postfix)] # e.g. "www" - if '.' not in prefix: + if "." not in prefix: to_remove.add(other) return sorted(set(domains) - to_remove) @@ -390,12 +360,12 @@ def _cascade_domains(self, domains): @api.model def _get_altnames(self): """Get the configured altnames as a list of strings.""" - parameter = self.env['ir.config_parameter'] + parameter = self.env["ir.config_parameter"] altnames = parameter.get_param("letsencrypt.altnames") if not altnames: base_url = parameter.get_param("web.base.url", "http://localhost") return [urllib.parse.urlparse(base_url).hostname] - return re.split('(?:,|\n| |;)+', altnames) + return re.split("(?:,|\n| |;)+", altnames) @api.model def _respond_challenge_http(self, challenge, account_key): @@ -404,7 +374,7 @@ def _respond_challenge_http(self, challenge, account_key): """ token = self._base64_encode(challenge.token) challenge_file = os.path.join(_get_challenge_dir(), token) - with open(challenge_file, 'w') as file_: + with open(challenge_file, "w") as file_: file_.write(challenge.validation(account_key)) @api.model @@ -413,9 +383,7 @@ def _respond_challenge_dns(self, domain, token): Respond to the DNS challenge by creating the DNS record on the provider. """ - provider = self.env['ir.config_parameter'].get_param( - 'letsencrypt.dns_provider' - ) + provider = self.env["ir.config_parameter"].get_param("letsencrypt.dns_provider") if not provider: raise UserError( _("No DNS provider set, can't request wildcard certificate") @@ -441,9 +409,7 @@ def _call_cmdline(self, cmdline, env=None): _logger.warning(stdout) if stderr: _logger.warning(stderr) - raise UserError( - _('Error calling %s: %d') % (cmdline, process.returncode) - ) + raise UserError(_("Error calling %s: %d") % (cmdline, process.returncode)) if stdout: _logger.info(stdout) if stderr: @@ -452,20 +418,17 @@ def _call_cmdline(self, cmdline, env=None): @api.model def _respond_challenge_dns_shell(self, domain, token): """Respond to a DNS challenge using an arbitrary shell command.""" - script_str = self.env['ir.config_parameter'].get_param( - 'letsencrypt.dns_shell_script' + script_str = self.env["ir.config_parameter"].get_param( + "letsencrypt.dns_shell_script" ) if script_str: env = os.environ.copy() env.update( - LETSENCRYPT_DNS_DOMAIN=domain, - LETSENCRYPT_DNS_CHALLENGE=token, + LETSENCRYPT_DNS_DOMAIN=domain, LETSENCRYPT_DNS_CHALLENGE=token, ) self._call_cmdline(script_str, env=env) else: - raise UserError( - _("No shell command configured for updating DNS records") - ) + raise UserError(_("No shell command configured for updating DNS records")) @api.model def _base64_encode(self, data): @@ -475,4 +438,4 @@ def _base64_encode(self, data): https://github.com/ietf-wg-acme/acme/issues/64#issuecomment-168852757 and https://golang.org/pkg/encoding/base64/#RawURLEncoding """ - return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii') + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") diff --git a/letsencrypt/models/res_config_settings.py b/letsencrypt/models/res_config_settings.py index 530ea81aa25..327ff55913c 100644 --- a/letsencrypt/models/res_config_settings.py +++ b/letsencrypt/models/res_config_settings.py @@ -3,7 +3,6 @@ from odoo import _, api, exceptions, fields, models - DNS_SCRIPT_DEFAULT = """# Write your script here # It should create a TXT record of $LETSENCRYPT_DNS_CHALLENGE # on _acme-challenge.$LETSENCRYPT_DNS_DOMAIN @@ -11,46 +10,45 @@ class ResConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' + _inherit = "res.config.settings" letsencrypt_altnames = fields.Text( string="Domain names", - default='', + default="", help=( - 'Domains to use for the certificate. ' - 'Separate with commas or newlines.' + "Domains to use for the certificate. " "Separate with commas or newlines." ), force_config_parameter="letsencrypt.altnames", ) letsencrypt_dns_provider = fields.Selection( - selection=[('shell', 'Shell script')], - string='DNS provider', + selection=[("shell", "Shell script")], + string="DNS provider", help=( - 'For wildcard certificates we need to add a TXT record on your ' + "For wildcard certificates we need to add a TXT record on your " 'DNS. If you set this to "Shell script" you can enter a shell ' - 'script. Other options can be added by installing additional ' - 'modules.' + "script. Other options can be added by installing additional " + "modules." ), config_parameter="letsencrypt.dns_provider", ) letsencrypt_dns_shell_script = fields.Text( - string='DNS update script', + string="DNS update script", help=( - 'Write a shell script that will update your DNS TXT records. ' - 'You can use the $LETSENCRYPT_DNS_CHALLENGE and ' - '$LETSENCRYPT_DNS_DOMAIN variables.' + "Write a shell script that will update your DNS TXT records. " + "You can use the $LETSENCRYPT_DNS_CHALLENGE and " + "$LETSENCRYPT_DNS_DOMAIN variables." ), default=DNS_SCRIPT_DEFAULT, force_config_parameter="letsencrypt.dns_shell_script", ) letsencrypt_needs_dns_provider = fields.Boolean() letsencrypt_reload_command = fields.Text( - string='Server reload command', - help='Fill this with the command to restart your web server.', + string="Server reload command", + help="Fill this with the command to restart your web server.", force_config_parameter="letsencrypt.reload_command", ) letsencrypt_testing_mode = fields.Boolean( - string='Use testing server', + string="Use testing server", help=( "Use the Let's Encrypt staging server, which has higher rate " "limits but doesn't create valid certificates." @@ -66,9 +64,9 @@ class ResConfigSettings(models.TransientModel): config_parameter="letsencrypt.prefer_dns", ) - @api.onchange('letsencrypt_altnames', 'letsencrypt_prefer_dns') + @api.onchange("letsencrypt_altnames", "letsencrypt_prefer_dns") def letsencrypt_check_dns_required(self): - altnames = self.letsencrypt_altnames or '' + altnames = self.letsencrypt_altnames or "" self.letsencrypt_needs_dns_provider = ( "*." in altnames or self.letsencrypt_prefer_dns ) @@ -77,8 +75,7 @@ def letsencrypt_check_dns_required(self): def default_get(self, fields_list): res = super().default_get(fields_list) res["letsencrypt_needs_dns_provider"] = ( - "*." in res["letsencrypt_altnames"] - or res["letsencrypt_prefer_dns"] + "*." in res["letsencrypt_altnames"] or res["letsencrypt_prefer_dns"] ) return res @@ -88,12 +85,11 @@ def set_values(self): self.letsencrypt_check_dns_required() - if self.letsencrypt_dns_provider == 'shell': + if self.letsencrypt_dns_provider == "shell": lines = [ - line.strip() - for line in self.letsencrypt_dns_shell_script.split('\n') + line.strip() for line in self.letsencrypt_dns_shell_script.split("\n") ] - if all(line == '' or line.startswith('#') for line in lines): + if all(line == "" or line.startswith("#") for line in lines): raise exceptions.ValidationError( _("You didn't write a DNS update script!") ) @@ -114,9 +110,7 @@ def _get_classified_fields(self): for name in classified["other"]: field = self._fields[name] if hasattr(field, "force_config_parameter"): - classified["config"].append( - (name, field.force_config_parameter) - ) + classified["config"].append((name, field.force_config_parameter)) else: new_other.append(name) classified["other"] = new_other diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index 4e43c1f711c..ebf420b511c 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -3,7 +3,6 @@ import os import shutil - from datetime import datetime, timedelta from os import path @@ -12,79 +11,76 @@ from odoo.exceptions import UserError, ValidationError from odoo.tests import SingleTransactionCase +from ..models.letsencrypt import _get_challenge_dir, _get_data_dir + try: import dns.resolver except ImportError: pass -from ..models.letsencrypt import _get_data_dir, _get_challenge_dir - -CERT_DIR = path.join(path.dirname(__file__), 'certs') +CERT_DIR = path.join(path.dirname(__file__), "certs") def _poll(order, deadline): - order_resource = mock.Mock(['fullchain_pem']) - order_resource.fullchain_pem = 'chain' + order_resource = mock.Mock(["fullchain_pem"]) + order_resource.fullchain_pem = "chain" return order_resource class TestLetsencrypt(SingleTransactionCase): def setUp(self): super().setUp() - self.env['ir.config_parameter'].set_param( - 'web.base.url', 'http://www.example.com' + self.env["ir.config_parameter"].set_param( + "web.base.url", "http://www.example.com" ) - self.env['res.config.settings'].create( + self.env["res.config.settings"].create( { - 'letsencrypt_dns_provider': 'shell', - 'letsencrypt_dns_shell_script': 'touch /tmp/.letsencrypt_test', - 'letsencrypt_altnames': 'www.example.com,*.example.com', - 'letsencrypt_reload_command': 'echo reloaded', + "letsencrypt_dns_provider": "shell", + "letsencrypt_dns_shell_script": "touch /tmp/.letsencrypt_test", + "letsencrypt_altnames": "www.example.com,*.example.com", + "letsencrypt_reload_command": "echo reloaded", } ).set_values() def test_config_settings(self): - setting_vals = self.env['res.config.settings'].default_get([]) - self.assertEqual(setting_vals['letsencrypt_dns_provider'], 'shell') + setting_vals = self.env["res.config.settings"].default_get([]) + self.assertEqual(setting_vals["letsencrypt_dns_provider"], "shell") self.assertEqual( - setting_vals['letsencrypt_dns_shell_script'], - 'touch /tmp/.letsencrypt_test', + setting_vals["letsencrypt_dns_shell_script"], + "touch /tmp/.letsencrypt_test", ) self.assertEqual( - setting_vals['letsencrypt_altnames'], - 'www.example.com,*.example.com' + setting_vals["letsencrypt_altnames"], "www.example.com,*.example.com" ) - self.assertEqual(setting_vals['letsencrypt_reload_command'], 'echo reloaded') - self.assertTrue(setting_vals['letsencrypt_needs_dns_provider']) - self.assertFalse(setting_vals['letsencrypt_prefer_dns']) + self.assertEqual(setting_vals["letsencrypt_reload_command"], "echo reloaded") + self.assertTrue(setting_vals["letsencrypt_needs_dns_provider"]) + self.assertFalse(setting_vals["letsencrypt_prefer_dns"]) with self.assertRaises(ValidationError): self.env["res.config.settings"].create( {"letsencrypt_dns_shell_script": "# Empty script"} ).set_values() - @mock.patch('acme.client.ClientV2.answer_challenge') - @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=_poll) + @mock.patch("acme.client.ClientV2.answer_challenge") + @mock.patch("acme.client.ClientV2.poll_and_finalize", side_effect=_poll) def test_http_challenge(self, poll, _answer_challenge): - letsencrypt = self.env['letsencrypt'] - self.env['res.config.settings'].create( - {'letsencrypt_altnames': ''} + letsencrypt = self.env["letsencrypt"] + self.env["res.config.settings"].create( + {"letsencrypt_altnames": ""} ).set_values() letsencrypt._cron() poll.assert_called() self.assertTrue(os.listdir(_get_challenge_dir())) - self.assertFalse(path.isfile('/tmp/.letsencrypt_test')) - self.assertTrue( - path.isfile(path.join(_get_data_dir(), 'www.example.com.crt')) - ) + self.assertFalse(path.isfile("/tmp/.letsencrypt_test")) + self.assertTrue(path.isfile(path.join(_get_data_dir(), "www.example.com.crt"))) # pylint: disable=unused-argument - @mock.patch('odoo.addons.letsencrypt.models.letsencrypt.DNSUpdate') - @mock.patch('dns.resolver.query') - @mock.patch('time.sleep') - @mock.patch('acme.client.ClientV2.answer_challenge') - @mock.patch('acme.client.ClientV2.poll_and_finalize', side_effect=_poll) + @mock.patch("odoo.addons.letsencrypt.models.letsencrypt.DNSUpdate") + @mock.patch("dns.resolver.query") + @mock.patch("time.sleep") + @mock.patch("acme.client.ClientV2.answer_challenge") + @mock.patch("acme.client.ClientV2.poll_and_finalize", side_effect=_poll) def test_dns_challenge(self, poll, answer_challenge, sleep, query, dnsupd): record = None @@ -120,204 +116,192 @@ def query_effect(domain, rectype): query.side_effect = query_effect self.install_certificate(days_left=10) - self.env['letsencrypt']._cron() + self.env["letsencrypt"]._cron() poll.assert_called() self.assertEqual(ncalls, 3) - self.assertTrue(path.isfile('/tmp/.letsencrypt_test')) - self.assertTrue( - path.isfile(path.join(_get_data_dir(), 'www.example.com.crt')) - ) + self.assertTrue(path.isfile("/tmp/.letsencrypt_test")) + self.assertTrue(path.isfile(path.join(_get_data_dir(), "www.example.com.crt"))) def test_dns_challenge_error_on_missing_provider(self): - self.env['res.config.settings'].create( + self.env["res.config.settings"].create( { - 'letsencrypt_altnames': '*.example.com', - 'letsencrypt_dns_provider': False, + "letsencrypt_altnames": "*.example.com", + "letsencrypt_dns_provider": False, } ).set_values() with self.assertRaises(UserError): - self.env['letsencrypt']._cron() + self.env["letsencrypt"]._cron() def test_prefer_dns_setting(self): - self.env['res.config.settings'].create( - { - 'letsencrypt_altnames': 'example.com', - 'letsencrypt_prefer_dns': True, - } + self.env["res.config.settings"].create( + {"letsencrypt_altnames": "example.com", "letsencrypt_prefer_dns": True} ).set_values() # pylint: disable=no-value-for-parameter self.test_dns_challenge() def test_cascading(self): - cascade = self.env['letsencrypt']._cascade_domains + cascade = self.env["letsencrypt"]._cascade_domains self.assertEqual( cascade( [ - 'www.example.com', - '*.example.com', - 'example.com', - 'example.com', - 'notexample.com', - 'multi.sub.example.com', - 'www2.example.com', - 'unrelated.com', + "www.example.com", + "*.example.com", + "example.com", + "example.com", + "notexample.com", + "multi.sub.example.com", + "www2.example.com", + "unrelated.com", ] ), [ - '*.example.com', - 'example.com', - 'multi.sub.example.com', - 'notexample.com', - 'unrelated.com', + "*.example.com", + "example.com", + "multi.sub.example.com", + "notexample.com", + "unrelated.com", ], ) self.assertEqual(cascade([]), []) - self.assertEqual(cascade(['*.example.com']), ['*.example.com']) - self.assertEqual(cascade(['www.example.com']), ['www.example.com']) + self.assertEqual(cascade(["*.example.com"]), ["*.example.com"]) + self.assertEqual(cascade(["www.example.com"]), ["www.example.com"]) self.assertEqual( - cascade(['www.example.com', 'example.com']), - ['example.com', 'www.example.com'], + cascade(["www.example.com", "example.com"]), + ["example.com", "www.example.com"], ) with self.assertRaises(UserError): - cascade(['www.*.example.com']) + cascade(["www.*.example.com"]) with self.assertRaises(UserError): - cascade(['*.*.example.com']) + cascade(["*.*.example.com"]) def test_altnames_parsing(self): - config = self.env['ir.config_parameter'] - letsencrypt = self.env['letsencrypt'] + config = self.env["ir.config_parameter"] + letsencrypt = self.env["letsencrypt"] self.assertEqual( - letsencrypt._get_altnames(), - ['www.example.com', '*.example.com'] + letsencrypt._get_altnames(), ["www.example.com", "*.example.com"] ) - config.set_param('letsencrypt.altnames', '') - self.assertEqual(letsencrypt._get_altnames(), ['www.example.com']) + config.set_param("letsencrypt.altnames", "") + self.assertEqual(letsencrypt._get_altnames(), ["www.example.com"]) - config.set_param('letsencrypt.altnames', 'foobar.example.com') - self.assertEqual(letsencrypt._get_altnames(), ['foobar.example.com']) + config.set_param("letsencrypt.altnames", "foobar.example.com") + self.assertEqual(letsencrypt._get_altnames(), ["foobar.example.com"]) - config.set_param( - 'letsencrypt.altnames', 'example.com,example.org,example.net' - ) + config.set_param("letsencrypt.altnames", "example.com,example.org,example.net") self.assertEqual( - letsencrypt._get_altnames(), - ['example.com', 'example.org', 'example.net'], + letsencrypt._get_altnames(), ["example.com", "example.org", "example.net"], ) config.set_param( - 'letsencrypt.altnames', 'example.com, example.org\nexample.net' + "letsencrypt.altnames", "example.com, example.org\nexample.net" ) self.assertEqual( - letsencrypt._get_altnames(), - ['example.com', 'example.org', 'example.net'], + letsencrypt._get_altnames(), ["example.com", "example.org", "example.net"], ) def test_key_generation_and_retrieval(self): - key_a1 = self.env['letsencrypt']._get_key('a.key') - key_a2 = self.env['letsencrypt']._get_key('a.key') - key_b = self.env['letsencrypt']._get_key('b.key') + key_a1 = self.env["letsencrypt"]._get_key("a.key") + key_a2 = self.env["letsencrypt"]._get_key("a.key") + key_b = self.env["letsencrypt"]._get_key("b.key") self.assertIsInstance(key_a1, bytes) self.assertIsInstance(key_a2, bytes) self.assertIsInstance(key_b, bytes) - self.assertTrue(path.isfile(path.join(_get_data_dir(), 'a.key'))) + self.assertTrue(path.isfile(path.join(_get_data_dir(), "a.key"))) self.assertEqual(key_a1, key_a2) self.assertNotEqual(key_a1, key_b) - @mock.patch('os.remove', side_effect=os.remove) + @mock.patch("os.remove", side_effect=os.remove) @mock.patch( - 'odoo.addons.letsencrypt.models.letsencrypt.Letsencrypt._generate_key', + "odoo.addons.letsencrypt.models.letsencrypt.Letsencrypt._generate_key", side_effect=lambda: None, ) def test_interrupted_key_writing(self, generate_key, remove): with self.assertRaises(TypeError): - self.env['letsencrypt']._get_key('a.key') - self.assertFalse(path.isfile(path.join(_get_data_dir(), 'a.key'))) + self.env["letsencrypt"]._get_key("a.key") + self.assertFalse(path.isfile(path.join(_get_data_dir(), "a.key"))) remove.assert_called() generate_key.assert_called() def test_domain_validation(self): - self.env['letsencrypt']._validate_domain('example.com') - self.env['letsencrypt']._validate_domain('www.example.com') + self.env["letsencrypt"]._validate_domain("example.com") + self.env["letsencrypt"]._validate_domain("www.example.com") with self.assertRaises(UserError): - self.env['letsencrypt']._validate_domain('1.1.1.1') + self.env["letsencrypt"]._validate_domain("1.1.1.1") with self.assertRaises(UserError): - self.env['letsencrypt']._validate_domain('192.168.1.1') + self.env["letsencrypt"]._validate_domain("192.168.1.1") with self.assertRaises(UserError): - self.env['letsencrypt']._validate_domain('localhost.localdomain') + self.env["letsencrypt"]._validate_domain("localhost.localdomain") with self.assertRaises(UserError): - self.env['letsencrypt']._validate_domain('testdomain') + self.env["letsencrypt"]._validate_domain("testdomain") with self.assertRaises(UserError): - self.env['letsencrypt']._validate_domain('::1') + self.env["letsencrypt"]._validate_domain("::1") def test_young_certificate(self): self.install_certificate(60) self.assertFalse( - self.env['letsencrypt']._should_run( - path.join(_get_data_dir(), 'www.example.com.crt'), - ['www.example.com', '*.example.com'], + self.env["letsencrypt"]._should_run( + path.join(_get_data_dir(), "www.example.com.crt"), + ["www.example.com", "*.example.com"], ) ) def test_old_certificate(self): self.install_certificate(20) self.assertTrue( - self.env['letsencrypt']._should_run( - path.join(_get_data_dir(), 'www.example.com.crt'), - ['www.example.com', '*.example.com'], + self.env["letsencrypt"]._should_run( + path.join(_get_data_dir(), "www.example.com.crt"), + ["www.example.com", "*.example.com"], ) ) def test_expired_certificate(self): self.install_certificate(-10) self.assertTrue( - self.env['letsencrypt']._should_run( - path.join(_get_data_dir(), 'www.example.com.crt'), - ['www.example.com', '*.example.com'], + self.env["letsencrypt"]._should_run( + path.join(_get_data_dir(), "www.example.com.crt"), + ["www.example.com", "*.example.com"], ) ) def test_missing_certificate(self): self.assertTrue( - self.env['letsencrypt']._should_run( - path.join(_get_data_dir(), 'www.example.com.crt'), - ['www.example.com', '*.example.com'], + self.env["letsencrypt"]._should_run( + path.join(_get_data_dir(), "www.example.com.crt"), + ["www.example.com", "*.example.com"], ) ) def test_new_altnames(self): - self.install_certificate(60, 'www.example.com', ()) + self.install_certificate(60, "www.example.com", ()) self.assertTrue( - self.env['letsencrypt']._should_run( - path.join(_get_data_dir(), 'www.example.com.crt'), - ['www.example.com', '*.example.com'], + self.env["letsencrypt"]._should_run( + path.join(_get_data_dir(), "www.example.com.crt"), + ["www.example.com", "*.example.com"], ) ) self.assertFalse( - self.env['letsencrypt']._should_run( - path.join(_get_data_dir(), 'www.example.com.crt'), - ['www.example.com'], + self.env["letsencrypt"]._should_run( + path.join(_get_data_dir(), "www.example.com.crt"), ["www.example.com"], ) ) def test_legacy_certificate_without_altnames(self): self.install_certificate(60, use_altnames=False) self.assertFalse( - self.env['letsencrypt']._should_run( - path.join(_get_data_dir(), 'www.example.com.crt'), - ['www.example.com'], + self.env["letsencrypt"]._should_run( + path.join(_get_data_dir(), "www.example.com.crt"), ["www.example.com"], ) ) def install_certificate( self, days_left, - common_name='www.example.com', - altnames=('*.example.com',), + common_name="www.example.com", + altnames=("*.example.com",), use_altnames=True, ): from cryptography import x509 @@ -334,14 +318,10 @@ def install_certificate( cert_builder = ( x509.CertificateBuilder() .subject_name( - x509.Name( - [x509.NameAttribute(x509.NameOID.COMMON_NAME, common_name)] - ) + x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, common_name)]) ) .issuer_name( - x509.Name( - [x509.NameAttribute(x509.NameOID.COMMON_NAME, 'myca.biz')] - ) + x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, "myca.biz")]) ) .not_valid_before(not_before) .not_valid_after(not_after) @@ -359,12 +339,12 @@ def install_certificate( ) cert = cert_builder.sign(key, hashes.SHA256(), default_backend()) - cert_file = path.join(_get_data_dir(), '%s.crt' % common_name) - with open(cert_file, 'wb') as file_: + cert_file = path.join(_get_data_dir(), "%s.crt" % common_name) + with open(cert_file, "wb") as file_: file_.write(cert.public_bytes(serialization.Encoding.PEM)) def tearDown(self): super().tearDown() shutil.rmtree(_get_data_dir(), ignore_errors=True) - if path.isfile('/tmp/.letsencrypt_test'): - os.remove('/tmp/.letsencrypt_test') + if path.isfile("/tmp/.letsencrypt_test"): + os.remove("/tmp/.letsencrypt_test") diff --git a/letsencrypt/views/res_config_settings.xml b/letsencrypt/views/res_config_settings.xml index eacc48d8d76..9bf509e62bd 100644 --- a/letsencrypt/views/res_config_settings.xml +++ b/letsencrypt/views/res_config_settings.xml @@ -2,68 +2,89 @@ Letsencrypt settings view res.config.settings - + -
      +

      Let's Encrypt

      + invisible="1" + />
      -
      -
      -
      -
      +
      -
      -
      From fe237b727202714d89caf95d0f018fcf51f9d2fb Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Thu, 19 Nov 2020 15:57:20 +0100 Subject: [PATCH 19/57] [FIX] letsencrypt: adapt to conventions. Get rid of unneeded decorators; Remove copyright lines from init files; Replace copyright symbol with text; Use https: where possible. --- letsencrypt/__init__.py | 3 +-- letsencrypt/__manifest__.py | 4 +++- letsencrypt/controllers/__init__.py | 3 +-- letsencrypt/controllers/main.py | 8 ++++---- letsencrypt/hooks.py | 4 ++-- letsencrypt/models/__init__.py | 3 +-- letsencrypt/models/letsencrypt.py | 9 --------- letsencrypt/models/res_config_settings.py | 7 ++----- letsencrypt/tests/__init__.py | 3 +-- letsencrypt/tests/test_http.py | 4 ++-- letsencrypt/tests/test_letsencrypt.py | 4 ++-- 11 files changed, 19 insertions(+), 33 deletions(-) diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index a89dc31af16..6bfb0d85391 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,5 +1,4 @@ -# © 2016 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import models from . import controllers from .hooks import post_init_hook diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index 8053da73145..fc2a30c26c5 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -16,5 +16,7 @@ "demo": ["demo/ir_cron.xml"], "post_init_hook": "post_init_hook", "installable": True, - "external_dependencies": {"python": ["acme", "cryptography", "dns", "josepy"]}, + "external_dependencies": { + "python": ["acme", "cryptography", "dnspython", "josepy"] + }, } diff --git a/letsencrypt/controllers/__init__.py b/letsencrypt/controllers/__init__.py index 685eb0d7120..ce9b7f1e668 100644 --- a/letsencrypt/controllers/__init__.py +++ b/letsencrypt/controllers/__init__.py @@ -1,3 +1,2 @@ -# © 2016 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import main diff --git a/letsencrypt/controllers/main.py b/letsencrypt/controllers/main.py index 2dfbc01a17f..f050c1d1bde 100644 --- a/letsencrypt/controllers/main.py +++ b/letsencrypt/controllers/main.py @@ -1,7 +1,7 @@ -# © 2016 Therp BV -# © 2016 Antonio Espinosa -# © 2018 Ignacio Ibeas -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2016 Therp BV . +# Copyright 2016 Antonio Espinosa . +# Copyright 2018 Ignacio Ibeas . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import os from odoo import http diff --git a/letsencrypt/hooks.py b/letsencrypt/hooks.py index a236a41e858..bded105e11e 100644 --- a/letsencrypt/hooks.py +++ b/letsencrypt/hooks.py @@ -1,5 +1,5 @@ -# © 2016 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2016-2020 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import SUPERUSER_ID, api diff --git a/letsencrypt/models/__init__.py b/letsencrypt/models/__init__.py index 54285a070df..3e26c5f2151 100644 --- a/letsencrypt/models/__init__.py +++ b/letsencrypt/models/__init__.py @@ -1,4 +1,3 @@ -# © 2016 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import letsencrypt from . import res_config_settings diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 2df4c9bef7b..42964c7a035 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -74,7 +74,6 @@ class Letsencrypt(models.AbstractModel): _name = "letsencrypt" _description = "Abstract model providing functions for letsencrypt" - @api.model def _generate_key(self): """Generate an entirely new key.""" return rsa.generate_private_key( @@ -87,7 +86,6 @@ def _generate_key(self): encryption_algorithm=serialization.NoEncryption(), ) - @api.model def _get_key(self, key_name): """Get a key for a filename, generating if if it doesn't exist.""" key_file = os.path.join(_get_data_dir(), key_name) @@ -109,7 +107,6 @@ def _get_key(self, key_name): key_bytes = file_.read() return key_bytes - @api.model def _validate_domain(self, domain): """Validate that a domain is publicly accessible.""" if ":" in domain or all(char.isdigit() or char == "." for char in domain): @@ -122,7 +119,6 @@ def _validate_domain(self, domain): _("Domain %s: Let's encrypt doesn't work with local domains!") % domain ) - @api.model def _should_run(self, cert_file, domains): """Inspect the existing certificate to see if action is necessary.""" domains = set(domains) @@ -290,7 +286,6 @@ def _cron(self): else: _logger.warning("No reload command defined.") - @api.model def _wait_for_record(self, domain, token): """Wait until a TXT record for a domain is visible.""" if not domain.endswith("."): @@ -330,7 +325,6 @@ def _create_client(self, account_key): net = acme.client.ClientNetwork(account_key) return acme.client.ClientV2(directory_json, net) - @api.model def _cascade_domains(self, domains): """Remove domains that are obsoleted by wildcard domains in the list. @@ -367,7 +361,6 @@ def _get_altnames(self): return [urllib.parse.urlparse(base_url).hostname] return re.split("(?:,|\n| |;)+", altnames) - @api.model def _respond_challenge_http(self, challenge, account_key): """ Respond to the HTTP challenge by writing the file to serve. @@ -391,7 +384,6 @@ def _respond_challenge_dns(self, domain, token): dns_function = getattr(self, "_respond_challenge_dns_" + provider) dns_function(domain.replace("*.", ""), token) - @api.model def _call_cmdline(self, cmdline, env=None): """Call a shell command.""" process = subprocess.Popen( @@ -430,7 +422,6 @@ def _respond_challenge_dns_shell(self, domain, token): else: raise UserError(_("No shell command configured for updating DNS records")) - @api.model def _base64_encode(self, data): """Encode data as a URL-safe base64 string without padding. diff --git a/letsencrypt/models/res_config_settings.py b/letsencrypt/models/res_config_settings.py index 327ff55913c..25bb33cbffa 100644 --- a/letsencrypt/models/res_config_settings.py +++ b/letsencrypt/models/res_config_settings.py @@ -1,5 +1,5 @@ -# Copyright 2018 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2018-2020 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import _, api, exceptions, fields, models @@ -79,12 +79,9 @@ def default_get(self, fields_list): ) return res - @api.multi def set_values(self): super().set_values() - self.letsencrypt_check_dns_required() - if self.letsencrypt_dns_provider == "shell": lines = [ line.strip() for line in self.letsencrypt_dns_shell_script.split("\n") diff --git a/letsencrypt/tests/__init__.py b/letsencrypt/tests/__init__.py index f5551bae996..78478dcab29 100644 --- a/letsencrypt/tests/__init__.py +++ b/letsencrypt/tests/__init__.py @@ -1,4 +1,3 @@ -# Copyright 2018 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import test_http from . import test_letsencrypt diff --git a/letsencrypt/tests/test_http.py b/letsencrypt/tests/test_http.py index d28032b46ce..57f55643b99 100644 --- a/letsencrypt/tests/test_http.py +++ b/letsencrypt/tests/test_http.py @@ -1,5 +1,5 @@ -# Copyright 2020 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2020 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import os import shutil diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index ebf420b511c..45a8588562b 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -1,5 +1,5 @@ -# Copyright 2018 Therp BV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# Copyright 2018-2020 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import os import shutil From 262523c33bdb09302e681168917e111737dd0db6 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 7 Apr 2021 10:35:22 +0200 Subject: [PATCH 20/57] [IMP] letsencrypt: black, isort, prettier --- letsencrypt/models/letsencrypt.py | 13 ++++++------- letsencrypt/tests/test_letsencrypt.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 42964c7a035..4581267836a 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -26,15 +26,12 @@ import acme.crypto_util import acme.errors import acme.messages - + import dns.resolver + import josepy from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa - - import dns.resolver - - import josepy except ImportError as e: _logger.debug(e) @@ -166,7 +163,8 @@ def _should_run(self, cert_file, domains): missing = domains - names if missing: _logger.info( - "Found new domains %s, requesting new certificate", ", ".join(missing), + "Found new domains %s, requesting new certificate", + ", ".join(missing), ) return True @@ -416,7 +414,8 @@ def _respond_challenge_dns_shell(self, domain, token): if script_str: env = os.environ.copy() env.update( - LETSENCRYPT_DNS_DOMAIN=domain, LETSENCRYPT_DNS_CHALLENGE=token, + LETSENCRYPT_DNS_DOMAIN=domain, + LETSENCRYPT_DNS_CHALLENGE=token, ) self._call_cmdline(script_str, env=env) else: diff --git a/letsencrypt/tests/test_letsencrypt.py b/letsencrypt/tests/test_letsencrypt.py index 45a8588562b..b9b23b05d57 100644 --- a/letsencrypt/tests/test_letsencrypt.py +++ b/letsencrypt/tests/test_letsencrypt.py @@ -192,14 +192,16 @@ def test_altnames_parsing(self): config.set_param("letsencrypt.altnames", "example.com,example.org,example.net") self.assertEqual( - letsencrypt._get_altnames(), ["example.com", "example.org", "example.net"], + letsencrypt._get_altnames(), + ["example.com", "example.org", "example.net"], ) config.set_param( "letsencrypt.altnames", "example.com, example.org\nexample.net" ) self.assertEqual( - letsencrypt._get_altnames(), ["example.com", "example.org", "example.net"], + letsencrypt._get_altnames(), + ["example.com", "example.org", "example.net"], ) def test_key_generation_and_retrieval(self): @@ -285,7 +287,8 @@ def test_new_altnames(self): ) self.assertFalse( self.env["letsencrypt"]._should_run( - path.join(_get_data_dir(), "www.example.com.crt"), ["www.example.com"], + path.join(_get_data_dir(), "www.example.com.crt"), + ["www.example.com"], ) ) @@ -293,7 +296,8 @@ def test_legacy_certificate_without_altnames(self): self.install_certificate(60, use_altnames=False) self.assertFalse( self.env["letsencrypt"]._should_run( - path.join(_get_data_dir(), "www.example.com.crt"), ["www.example.com"], + path.join(_get_data_dir(), "www.example.com.crt"), + ["www.example.com"], ) ) From fa35a6b2e05f9a808ce0da92185429306cca1329 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 7 Apr 2021 13:12:30 +0200 Subject: [PATCH 21/57] [MIG] letsencrypt: Migration to 14.0 Including reorganization and reordering of letsencrypt methods, because of warning of to complicated _cron(..) method. --- letsencrypt/README.rst | 10 +- letsencrypt/__manifest__.py | 7 +- letsencrypt/models/letsencrypt.py | 351 +++++++++++----------- letsencrypt/static/description/index.html | 8 +- 4 files changed, 194 insertions(+), 182 deletions(-) diff --git a/letsencrypt/README.rst b/letsencrypt/README.rst index 3a04e6f2cee..be5abc8ae0a 100644 --- a/letsencrypt/README.rst +++ b/letsencrypt/README.rst @@ -14,13 +14,13 @@ Let's Encrypt :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github - :target: https://github.com/OCA/server-tools/tree/12.0/letsencrypt + :target: https://github.com/OCA/server-tools/tree/14.0/letsencrypt :alt: OCA/server-tools .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-letsencrypt + :target: https://translation.odoo-community.org/projects/server-tools-14-0/server-tools-14-0-letsencrypt :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/149/12.0 + :target: https://runbot.odoo-community.org/runbot/149/14.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -148,7 +148,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -199,6 +199,6 @@ 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. -This module is part of the `OCA/server-tools `_ project on GitHub. +This module is part of the `OCA/server-tools `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/letsencrypt/__manifest__.py b/letsencrypt/__manifest__.py index fc2a30c26c5..bc24ac41798 100644 --- a/letsencrypt/__manifest__.py +++ b/letsencrypt/__manifest__.py @@ -1,11 +1,12 @@ -# 2016-2020 Therp BV . +# 2016-2021 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Let's Encrypt", - "version": "13.0.1.0.0", + "version": "14.0.1.0.0", "author": "Therp BV," "Tecnativa," "Acysos S.L," "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-tools", "license": "AGPL-3", - "category": "Hidden/Dependency", + "category": "Tools", "summary": "Request SSL certificates from letsencrypt.org", "depends": ["base_setup"], "data": [ diff --git a/letsencrypt/models/letsencrypt.py b/letsencrypt/models/letsencrypt.py index 4581267836a..3f485e160a3 100644 --- a/letsencrypt/models/letsencrypt.py +++ b/letsencrypt/models/letsencrypt.py @@ -1,4 +1,4 @@ -# Copyright 2016-2020 Therp BV . +# Copyright 2016-2021 Therp BV . # Copyright 2016 Antonio Espinosa . # Copyright 2018 Ignacio Ibeas . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). @@ -71,38 +71,61 @@ class Letsencrypt(models.AbstractModel): _name = "letsencrypt" _description = "Abstract model providing functions for letsencrypt" - def _generate_key(self): - """Generate an entirely new key.""" - return rsa.generate_private_key( - public_exponent=65537, - key_size=DEFAULT_KEY_LENGTH, - backend=default_backend(), - ).private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) + @api.model + def _cron(self): + """Define cronjob to renew certificate when needed.""" + domains = self._get_altnames() + main_domain = domains[0] + domains = self._cascade_domains(domains) + for dom in domains: + self._validate_domain(dom) - def _get_key(self, key_name): - """Get a key for a filename, generating if if it doesn't exist.""" - key_file = os.path.join(_get_data_dir(), key_name) - if not os.path.isfile(key_file): - _logger.info("Generating new key %s", key_name) - key_bytes = self._generate_key() - try: - with open(key_file, "wb") as file_: - os.fchmod(file_.fileno(), 0o600) - file_.write(key_bytes) - except BaseException: - # An incomplete file would block generation of a new one - if os.path.isfile(key_file): - os.remove(key_file) - raise - else: - _logger.info("Getting existing key %s", key_name) - with open(key_file, "rb") as file_: - key_bytes = file_.read() - return key_bytes + cert_file = os.path.join(_get_data_dir(), "%s.crt" % main_domain) + if not self._should_run(cert_file, domains): + return + + account_key = josepy.JWKRSA.load(self._get_key("account.key")) + client = self._create_client(account_key) + self._register_client(client, account_key) + authzr = self._get_authorization_resource(client, main_domain, domains) + + self._handle_challenges(client, account_key, authzr) + order_resource = self._letsencrypt_validation(client, authzr) + self._save_and_reload(cert_file, order_resource) + + def _get_altnames(self): + """Get the configured altnames as a list of strings.""" + parameter = self.env["ir.config_parameter"] + altnames = parameter.get_param("letsencrypt.altnames") + if not altnames: + base_url = parameter.get_param("web.base.url", "http://localhost") + return [urllib.parse.urlparse(base_url).hostname] + return re.split("(?:,|\n| |;)+", altnames) + + def _cascade_domains(self, domains): + """Remove domains that are obsoleted by wildcard domains in the list. + + Requesting www.example.com is unnecessary if *.example.com is also + requested. example.com isn't obsoleted however, and neither is + sub.domain.example.com. + """ + to_remove = set() + for domain in domains: + if WILDCARD in domain[1:]: + raise UserError( + _("A wildcard is only allowed at the start of a domain") + ) + if domain.startswith(WILDCARD): + postfix = domain[1:] # e.g. ".example.com" + # This makes it O(n²) but n <= 100 so it's ok + for other in domains: + if other.startswith(WILDCARD): + continue + if other.endswith(postfix): + prefix = other[: -len(postfix)] # e.g. "www" + if "." not in prefix: + to_remove.add(other) + return sorted(set(domains) - to_remove) def _validate_domain(self, domain): """Validate that a domain is publicly accessible.""" @@ -110,7 +133,6 @@ def _validate_domain(self, domain): raise UserError( _("Domain %s: Let's Encrypt doesn't support IP addresses!") % domain ) - if domain in LOCAL_DOMAINS or "." not in domain: raise UserError( _("Domain %s: Let's encrypt doesn't work with local domains!") % domain @@ -118,8 +140,6 @@ def _validate_domain(self, domain): def _should_run(self, cert_file, domains): """Inspect the existing certificate to see if action is necessary.""" - domains = set(domains) - if not os.path.isfile(cert_file): _logger.info("No existing certificate found, creating a new one") return True @@ -160,6 +180,7 @@ def _should_run(self, cert_file, domains): except x509.extensions.ExtensionNotFound: pass + domains = set(domains) missing = domains - names if missing: _logger.info( @@ -167,7 +188,6 @@ def _should_run(self, cert_file, domains): ", ".join(missing), ) return True - _logger.info( "Certificate expires on %s, which is in %d days, no action needed", expiry, @@ -175,24 +195,52 @@ def _should_run(self, cert_file, domains): ) return False - @api.model - def _cron(self): - ir_config_parameter = self.env["ir.config_parameter"] - domains = self._get_altnames() - domain = domains[0] - cert_file = os.path.join(_get_data_dir(), "%s.crt" % domain) - - domains = self._cascade_domains(domains) - for dom in domains: - self._validate_domain(dom) + def _get_key(self, key_name): + """Get a key for a filename, generating if if it doesn't exist.""" + key_file = os.path.join(_get_data_dir(), key_name) + if not os.path.isfile(key_file): + _logger.info("Generating new key %s", key_name) + key_bytes = self._generate_key() + try: + with open(key_file, "wb") as file_: + os.fchmod(file_.fileno(), 0o600) + file_.write(key_bytes) + except BaseException: + # An incomplete file would block generation of a new one + if os.path.isfile(key_file): + os.remove(key_file) + raise + else: + _logger.info("Getting existing key %s", key_name) + with open(key_file, "rb") as file_: + key_bytes = file_.read() + return key_bytes - if not self._should_run(cert_file, domains): - return + def _generate_key(self): + """Generate an entirely new key.""" + return rsa.generate_private_key( + public_exponent=65537, + key_size=DEFAULT_KEY_LENGTH, + backend=default_backend(), + ).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) - account_key = josepy.JWKRSA.load(self._get_key("account.key")) - domain_key = self._get_key("%s.key" % domain) + def _create_client(self, account_key): + param = self.env["ir.config_parameter"] + testing_mode = param.get_param("letsencrypt.testing_mode") == "True" + if config["test_enable"] or testing_mode: + directory_url = V2_STAGING_DIRECTORY_URL + else: + directory_url = V2_DIRECTORY_URL + directory_json = requests.get(directory_url).json() + net = acme.client.ClientNetwork(account_key) + return acme.client.ClientV2(directory_json, net) - client = self._create_client(account_key) + def _register_client(self, client, account_key): + """Register this Letsencrypt client.""" new_reg = acme.messages.NewRegistration( key=account_key.public_key(), terms_of_service_agreed=True ) @@ -205,17 +253,23 @@ def _cron(self): client.query_registration(reg_res) _logger.info("Reusing existing account.") + def _get_authorization_resource(self, client, main_domain, domains): + """Get acme authorization_resource.""" + domain_key = self._get_key("%s.key" % main_domain) _logger.info("Making CSR for the following domains: %s", domains) csr = acme.crypto_util.make_csr(private_key_pem=domain_key, domains=domains) - authzr = client.new_order(csr) + return client.new_order(csr) - # For each requested domain name we receive a list of challenges. - # We only have to do one from each list. - # HTTP challenges are the easiest, so do one of those if possible. - # We can do DNS challenges too. There are other types that we don't - # support. - pending_responses = [] + def _handle_challenges(self, client, account_key, authzr): + """Handle challenges from the Letsencrypt provider. + For each requested domain name we receive a list of challenges. + We only have to do one from each list. + HTTP challenges are the easiest, so do one of those if possible. + We can do DNS challenges too. There are other types that we don't + support. + """ + pending_responses = [] prefer_dns = ( self.env["ir.config_parameter"].get_param("letsencrypt.prefer_dns") == "True" @@ -263,26 +317,51 @@ def _cron(self): for update in pending_responses: client.answer_challenge(update.challenge, acme.challenges.DNSResponse()) - # let them know we are done and they should check - backoff = int(ir_config_parameter.get_param("letsencrypt.backoff", 3)) - deadline = datetime.now() + timedelta(minutes=backoff) - try: - order_resource = client.poll_and_finalize(authzr, deadline) - except acme.errors.ValidationError as error: - _logger.error("Let's Encrypt validation failed!") - for authz in error.failed_authzrs: - for challenge in authz.body.challenges: - _logger.error(str(challenge.error)) - raise + def _respond_challenge_http(self, challenge, account_key): + """ + Respond to the HTTP challenge by writing the file to serve. + """ + token = self._base64_encode(challenge.token) + challenge_file = os.path.join(_get_challenge_dir(), token) + with open(challenge_file, "w") as file_: + file_.write(challenge.validation(account_key)) - with open(cert_file, "w") as crt: - crt.write(order_resource.fullchain_pem) - _logger.info("SUCCESS: Certificate saved: %s", cert_file) - reload_cmd = ir_config_parameter.get_param("letsencrypt.reload_command", "") - if reload_cmd.strip(): - self._call_cmdline(reload_cmd) + def _respond_challenge_dns(self, domain, token): + """ + Respond to the DNS challenge by creating the DNS record + on the provider. + """ + provider = self.env["ir.config_parameter"].get_param("letsencrypt.dns_provider") + if not provider: + raise UserError( + _("No DNS provider set, can't request wildcard certificate") + ) + dns_function = getattr(self, "_respond_challenge_dns_" + provider) + dns_function(domain.replace("*.", ""), token) + + def _respond_challenge_dns_shell(self, domain, token): + """Respond to a DNS challenge using an arbitrary shell command.""" + script_str = self.env["ir.config_parameter"].get_param( + "letsencrypt.dns_shell_script" + ) + if script_str: + env = os.environ.copy() + env.update( + LETSENCRYPT_DNS_DOMAIN=domain, + LETSENCRYPT_DNS_CHALLENGE=token, + ) + self._call_cmdline(script_str, env=env) else: - _logger.warning("No reload command defined.") + raise UserError(_("No shell command configured for updating DNS records")) + + def _base64_encode(self, data): + """Encode data as a URL-safe base64 string without padding. + + This should be the encoding that Let's Encrypt uses for all base64. See + https://github.com/ietf-wg-acme/acme/issues/64#issuecomment-168852757 + and https://golang.org/pkg/encoding/base64/#RawURLEncoding + """ + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") def _wait_for_record(self, domain, token): """Wait until a TXT record for a domain is visible.""" @@ -311,76 +390,33 @@ def _wait_for_record(self, domain, token): ) return - @api.model - def _create_client(self, account_key): - param = self.env["ir.config_parameter"] - testing_mode = param.get_param("letsencrypt.testing_mode") == "True" - if config["test_enable"] or testing_mode: - directory_url = V2_STAGING_DIRECTORY_URL - else: - directory_url = V2_DIRECTORY_URL - directory_json = requests.get(directory_url).json() - net = acme.client.ClientNetwork(account_key) - return acme.client.ClientV2(directory_json, net) - - def _cascade_domains(self, domains): - """Remove domains that are obsoleted by wildcard domains in the list. - - Requesting www.example.com is unnecessary if *.example.com is also - requested. example.com isn't obsoleted however, and neither is - sub.domain.example.com. - """ - to_remove = set() - for domain in domains: - if WILDCARD in domain[1:]: - raise UserError( - _("A wildcard is only allowed at the start of a domain") - ) - if domain.startswith(WILDCARD): - postfix = domain[1:] # e.g. ".example.com" - # This makes it O(n²) but n <= 100 so it's ok - for other in domains: - if other.startswith(WILDCARD): - continue - if other.endswith(postfix): - prefix = other[: -len(postfix)] # e.g. "www" - if "." not in prefix: - to_remove.add(other) - - return sorted(set(domains) - to_remove) - - @api.model - def _get_altnames(self): - """Get the configured altnames as a list of strings.""" - parameter = self.env["ir.config_parameter"] - altnames = parameter.get_param("letsencrypt.altnames") - if not altnames: - base_url = parameter.get_param("web.base.url", "http://localhost") - return [urllib.parse.urlparse(base_url).hostname] - return re.split("(?:,|\n| |;)+", altnames) - - def _respond_challenge_http(self, challenge, account_key): - """ - Respond to the HTTP challenge by writing the file to serve. - """ - token = self._base64_encode(challenge.token) - challenge_file = os.path.join(_get_challenge_dir(), token) - with open(challenge_file, "w") as file_: - file_.write(challenge.validation(account_key)) + def _letsencrypt_validation(self, client, authzr): + """Do the validation of the certificates.""" + ir_config_parameter = self.env["ir.config_parameter"] + # let them know we are done and they should check + backoff = int(ir_config_parameter.get_param("letsencrypt.backoff", 3)) + deadline = datetime.now() + timedelta(minutes=backoff) + try: + order_resource = client.poll_and_finalize(authzr, deadline) + except acme.errors.ValidationError as error: + _logger.error("Let's Encrypt validation failed!") + for authz in error.failed_authzrs: + for challenge in authz.body.challenges: + _logger.error(str(challenge.error)) + raise + return order_resource - @api.model - def _respond_challenge_dns(self, domain, token): - """ - Respond to the DNS challenge by creating the DNS record - on the provider. - """ - provider = self.env["ir.config_parameter"].get_param("letsencrypt.dns_provider") - if not provider: - raise UserError( - _("No DNS provider set, can't request wildcard certificate") - ) - dns_function = getattr(self, "_respond_challenge_dns_" + provider) - dns_function(domain.replace("*.", ""), token) + def _save_and_reload(self, cert_file, order_resource): + """Save certfile and reload nginx or other webserver.""" + ir_config_parameter = self.env["ir.config_parameter"] + with open(cert_file, "w") as crt: + crt.write(order_resource.fullchain_pem) + _logger.info("SUCCESS: Certificate saved: %s", cert_file) + reload_cmd = ir_config_parameter.get_param("letsencrypt.reload_command", "") + if reload_cmd.strip(): + self._call_cmdline(reload_cmd) + else: + _logger.warning("No reload command defined.") def _call_cmdline(self, cmdline, env=None): """Call a shell command.""" @@ -404,28 +440,3 @@ def _call_cmdline(self, cmdline, env=None): _logger.info(stdout) if stderr: _logger.info(stderr) - - @api.model - def _respond_challenge_dns_shell(self, domain, token): - """Respond to a DNS challenge using an arbitrary shell command.""" - script_str = self.env["ir.config_parameter"].get_param( - "letsencrypt.dns_shell_script" - ) - if script_str: - env = os.environ.copy() - env.update( - LETSENCRYPT_DNS_DOMAIN=domain, - LETSENCRYPT_DNS_CHALLENGE=token, - ) - self._call_cmdline(script_str, env=env) - else: - raise UserError(_("No shell command configured for updating DNS records")) - - def _base64_encode(self, data): - """Encode data as a URL-safe base64 string without padding. - - This should be the encoding that Let's Encrypt uses for all base64. See - https://github.com/ietf-wg-acme/acme/issues/64#issuecomment-168852757 - and https://golang.org/pkg/encoding/base64/#RawURLEncoding - """ - return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") diff --git a/letsencrypt/static/description/index.html b/letsencrypt/static/description/index.html index 79d84c26fad..d12429e73c2 100644 --- a/letsencrypt/static/description/index.html +++ b/letsencrypt/static/description/index.html @@ -3,7 +3,7 @@ - + Let's Encrypt