[MIG] Migrate letsencrypt 2.0.0 to Odoo 11.0

pull/2236/head
Jan Verbeek 2020-04-22 19:39:48 +02:00 committed by Ronald Portier
parent 1428743f50
commit 11623a869c
12 changed files with 121 additions and 71 deletions

View File

@ -14,13 +14,13 @@ Let's Encrypt
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3 :alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github .. |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 :alt: OCA/server-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png .. |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 :alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png .. |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 :alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5| |badge1| |badge2| |badge3| |badge4| |badge5|
@ -148,7 +148,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_. Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported. 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 If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20letsencrypt%0Aversion:%2010.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. `feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20letsencrypt%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues. Do not contact contributors directly about support or help with technical issues.
@ -160,6 +160,7 @@ Authors
* Therp BV * Therp BV
* Tecnativa * Tecnativa
* Acysos S.L
Contributors 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 mission is to support the collaborative development of Odoo features and
promote its widespread use. promote its widespread use.
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/10.0/letsencrypt>`_ project on GitHub. This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/11.0/letsencrypt>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -16,7 +16,7 @@
"data": [ "data": [
"data/ir_config_parameter.xml", "data/ir_config_parameter.xml",
"data/ir_cron.xml", "data/ir_cron.xml",
"views/base_config_settings.xml", "views/res_config_settings.xml",
], ],
"demo": [ "demo": [
"demo/ir_cron.xml", "demo/ir_cron.xml",

View File

@ -16,9 +16,6 @@
forcecreate="True"> forcecreate="True">
<field name="key">letsencrypt.backoff</field> <field name="key">letsencrypt.backoff</field>
<field name="value">3</field> <field name="value">3</field>
<field
name="group_ids"
eval="[(6, False, [ref('base.group_system')])]"/>
</record> </record>
</data> </data>

View File

@ -6,6 +6,10 @@ from odoo import api, SUPERUSER_ID
def migrate_altnames(env): def migrate_altnames(env):
config = env["ir.config_parameter"] config = env["ir.config_parameter"]
existing = config.search([("key", "=like", "letsencrypt.altname.%")]) 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")) new_domains = "\n".join(existing.mapped("value"))
config.set_param("letsencrypt.altnames", new_domains) config.set_param("letsencrypt.altnames", new_domains)
existing.unlink() existing.unlink()
@ -18,14 +22,18 @@ def migrate_cron(env):
jobs = ( jobs = (
env["ir.cron"] env["ir.cron"]
.with_context(active_test=False) .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: if not jobs:
# ir.cron._try_lock doesn't handle empty recordsets well # ir.cron._try_lock doesn't handle empty recordsets well
return return
jobs.write( jobs.write({"interval_type": "days", "interval_number": "1"})
{"function": "_cron", "interval_type": "days", "interval_number": "1"} jobs.mapped("ir_actions_server_id").write({"code": "model._cron()"})
)
def migrate(cr, version): def migrate(cr, version):

View File

@ -1,4 +1,4 @@
# © 2016 Therp BV <http://therp.nl> # © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import letsencrypt from . import letsencrypt
from . import base_config_settings from . import res_config_settings

View File

@ -10,7 +10,7 @@ import os
import re import re
import subprocess import subprocess
import time import time
import urlparse import urllib.parse
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -197,7 +197,7 @@ class Letsencrypt(models.AbstractModel):
def _cron(self): def _cron(self):
ir_config_parameter = self.env['ir.config_parameter'] ir_config_parameter = self.env['ir.config_parameter']
base_url = ir_config_parameter.get_param('web.base.url', 'localhost') 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) cert_file = os.path.join(_get_data_dir(), '%s.crt' % domain)
domains = self._cascade_domains([domain] + self._get_altnames()) domains = self._cascade_domains([domain] + self._get_altnames())
@ -403,7 +403,7 @@ class Letsencrypt(models.AbstractModel):
Respond to the HTTP challenge by writing the file to serve. Respond to the HTTP challenge by writing the file to serve.
""" """
token = self._base64_encode(challenge.token) 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_: with open(challenge_file, 'w') as file_:
file_.write(challenge.validation(account_key)) file_.write(challenge.validation(account_key))

View File

@ -10,8 +10,8 @@ DNS_SCRIPT_DEFAULT = """# Write your script here
""" """
class BaseConfigSettings(models.TransientModel): class ResConfigSettings(models.TransientModel):
_inherit = 'base.config.settings' _inherit = 'res.config.settings'
letsencrypt_altnames = fields.Text( letsencrypt_altnames = fields.Text(
string="Domain names", string="Domain names",
@ -69,7 +69,7 @@ class BaseConfigSettings(models.TransientModel):
@api.model @api.model
def default_get(self, fields_list): 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 get_param = self.env['ir.config_parameter'].get_param
res.update( res.update(
{ {
@ -97,8 +97,9 @@ class BaseConfigSettings(models.TransientModel):
return res return res
@api.multi @api.multi
def set_dns_provider(self): def set_values(self):
self.ensure_one() super().set_values()
self.letsencrypt_check_dns_required() self.letsencrypt_check_dns_required()
if self.letsencrypt_dns_provider == 'shell': if self.letsencrypt_dns_provider == 'shell':

View File

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

View File

@ -367,7 +367,7 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !! !! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !! !! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/server-tools/tree/10.0/letsencrypt"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/server-tools-10-0/server-tools-10-0-letsencrypt"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/149/10.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p> <p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/server-tools/tree/11.0/letsencrypt"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/server-tools-11-0/server-tools-11-0-letsencrypt"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/149/11.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module was written to have your Odoo installation request SSL certificates <p>This module was written to have your Odoo installation request SSL certificates
from <a class="reference external" href="https://letsencrypt.org">https://letsencrypt.org</a> automatically.</p> from <a class="reference external" href="https://letsencrypt.org">https://letsencrypt.org</a> automatically.</p>
<p><strong>Table of contents</strong></p> <p><strong>Table of contents</strong></p>
@ -490,7 +490,7 @@ For example, <tt class="docutils literal"><span class="pre">--load=web,letsencry
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>. <p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported. 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 If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/server-tools/issues/new?body=module:%20letsencrypt%0Aversion:%2010.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p> <a class="reference external" href="https://github.com/OCA/server-tools/issues/new?body=module:%20letsencrypt%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p> <p>Do not contact contributors directly about support or help with technical issues.</p>
</div> </div>
<div class="section" id="credits"> <div class="section" id="credits">
@ -500,6 +500,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<ul class="simple"> <ul class="simple">
<li>Therp BV</li> <li>Therp BV</li>
<li>Tecnativa</li> <li>Tecnativa</li>
<li>Acysos S.L</li>
</ul> </ul>
</div> </div>
<div class="section" id="contributors"> <div class="section" id="contributors">
@ -509,6 +510,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<li>Antonio Espinosa &lt;<a class="reference external" href="mailto:antonio.espinosa&#64;tecnativa.com">antonio.espinosa&#64;tecnativa.com</a>&gt;</li> <li>Antonio Espinosa &lt;<a class="reference external" href="mailto:antonio.espinosa&#64;tecnativa.com">antonio.espinosa&#64;tecnativa.com</a>&gt;</li>
<li>Dave Lasley &lt;<a class="reference external" href="mailto:dave&#64;laslabs.com">dave&#64;laslabs.com</a>&gt;</li> <li>Dave Lasley &lt;<a class="reference external" href="mailto:dave&#64;laslabs.com">dave&#64;laslabs.com</a>&gt;</li>
<li>Ronald Portier &lt;<a class="reference external" href="mailto:ronald&#64;therp.nl">ronald&#64;therp.nl</a>&gt;</li> <li>Ronald Portier &lt;<a class="reference external" href="mailto:ronald&#64;therp.nl">ronald&#64;therp.nl</a>&gt;</li>
<li>Ignacio Ibeas &lt;<a class="reference external" href="mailto:ignacio&#64;acysos.com">ignacio&#64;acysos.com</a>&gt;</li>
<li>George Daramouskas &lt;<a class="reference external" href="mailto:gdaramouskas&#64;therp.nl">gdaramouskas&#64;therp.nl</a>&gt;</li> <li>George Daramouskas &lt;<a class="reference external" href="mailto:gdaramouskas&#64;therp.nl">gdaramouskas&#64;therp.nl</a>&gt;</li>
<li>Jan Verbeek &lt;<a class="reference external" href="mailto:jverbeek&#64;therp.nl">jverbeek&#64;therp.nl</a>&gt;</li> <li>Jan Verbeek &lt;<a class="reference external" href="mailto:jverbeek&#64;therp.nl">jverbeek&#64;therp.nl</a>&gt;</li>
</ul> </ul>
@ -535,7 +537,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose <p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and mission is to support the collaborative development of Odoo features and
promote its widespread use.</p> promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/10.0/letsencrypt">OCA/server-tools</a> project on GitHub.</p> <p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/11.0/letsencrypt">OCA/server-tools</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p> <p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div> </div>
</div> </div>

View File

@ -1,8 +1,6 @@
# Copyright 2018 Therp BV <http://therp.nl> # Copyright 2018 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from __future__ import unicode_literals # cryptography is picky
import os import os
import shutil import shutil
@ -28,21 +26,21 @@ def _poll(order, deadline):
class TestLetsencrypt(SingleTransactionCase): class TestLetsencrypt(SingleTransactionCase):
def setUp(self): def setUp(self):
super(TestLetsencrypt, self).setUp() super().setUp()
self.env['ir.config_parameter'].set_param( self.env['ir.config_parameter'].set_param(
'web.base.url', 'http://www.example.com' '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_provider': 'shell',
'letsencrypt_dns_shell_script': 'touch /tmp/.letsencrypt_test', 'letsencrypt_dns_shell_script': 'touch /tmp/.letsencrypt_test',
'letsencrypt_altnames': '*.example.com', 'letsencrypt_altnames': '*.example.com',
'letsencrypt_reload_command': 'true', # i.e. /bin/true 'letsencrypt_reload_command': 'true', # i.e. /bin/true
} }
).set_dns_provider() ).set_values()
def test_config_settings(self): 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_provider'], 'shell')
self.assertEqual( self.assertEqual(
setting_vals['letsencrypt_dns_shell_script'], setting_vals['letsencrypt_dns_shell_script'],
@ -57,9 +55,9 @@ class TestLetsencrypt(SingleTransactionCase):
@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): def test_http_challenge(self, poll, answer_challenge):
letsencrypt = self.env['letsencrypt'] letsencrypt = self.env['letsencrypt']
self.env['base.config.settings'].create( self.env['res.config.settings'].create(
{'letsencrypt_altnames': 'test.example.com'} {'letsencrypt_altnames': 'test.example.com'}
).set_dns_provider() ).set_values()
letsencrypt._cron() letsencrypt._cron()
poll.assert_called() poll.assert_called()
self.assertTrue(os.listdir(_get_challenge_dir())) self.assertTrue(os.listdir(_get_challenge_dir()))
@ -98,22 +96,22 @@ class TestLetsencrypt(SingleTransactionCase):
) )
def test_dns_challenge_error_on_missing_provider(self): 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_altnames': '*.example.com',
'letsencrypt_dns_provider': False, 'letsencrypt_dns_provider': False,
} }
).set_dns_provider() ).set_values()
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.env['letsencrypt']._cron() self.env['letsencrypt']._cron()
def test_prefer_dns_setting(self): def test_prefer_dns_setting(self):
self.env['base.config.settings'].create( self.env['res.config.settings'].create(
{ {
'letsencrypt_altnames': 'example.com', 'letsencrypt_altnames': 'example.com',
'letsencrypt_prefer_dns': True, 'letsencrypt_prefer_dns': True,
} }
).set_dns_provider() ).set_values()
self.env['ir.config_parameter'].set_param( self.env['ir.config_parameter'].set_param(
'web.base.url', 'http://example.com' 'web.base.url', 'http://example.com'
) )
@ -327,7 +325,7 @@ class TestLetsencrypt(SingleTransactionCase):
file_.write(cert.public_bytes(serialization.Encoding.PEM)) file_.write(cert.public_bytes(serialization.Encoding.PEM))
def tearDown(self): def tearDown(self):
super(TestLetsencrypt, self).tearDown() super().tearDown()
shutil.rmtree(_get_data_dir(), ignore_errors=True) shutil.rmtree(_get_data_dir(), ignore_errors=True)
if path.isfile('/tmp/.letsencrypt_test'): if path.isfile('/tmp/.letsencrypt_test'):
os.remove('/tmp/.letsencrypt_test') os.remove('/tmp/.letsencrypt_test')

View File

@ -1,33 +0,0 @@
<odoo>
<record id="base_config_settings" model="ir.ui.view">
<field name="name">Letsencrypt base config settings</field>
<field name="model">base.config.settings</field>
<field name="inherit_id" ref="base_setup.view_general_configuration" />
<field name="arch" type="xml">
<xpath expr="./group[last()]" position="after">
<group name="letsencrypt">
<group name="letsencrypt" string="Let's Encrypt">
<field
name="letsencrypt_needs_dns_provider"
readonly="1"
invisible="1"/>
<field
name="letsencrypt_dns_provider"
attrs="{'required':
[('letsencrypt_needs_dns_provider', '=', True)]}" />
<field
name="letsencrypt_dns_shell_script"
attrs="{
'required': [('letsencrypt_dns_provider', '=', 'shell')],
'invisible': [('letsencrypt_dns_provider', '!=', 'shell')],
}" />
<field name="letsencrypt_altnames" />
<field name="letsencrypt_reload_command" />
<field name="letsencrypt_testing_mode" />
<field name="letsencrypt_prefer_dns" />
</group>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,75 @@
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">Letsencrypt settings view</field>
<field name="model">res.config.settings</field>
<field name="inherit_id"
ref="base_setup.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block"
data-string="Let's Encrypt"
string="Let's Encrypt"
data-key="letsencrypt">
<div id="letsencrypt_settings">
<h2>Let's Encrypt</h2>
<field
name="letsencrypt_needs_dns_provider"
readonly="1"
invisible="1"/>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-12 o_setting_box">
<div class="o_setting_right_pane">
<label for="letsencrypt_altnames"/>
<div class="text-muted">List additional domains for the certificate</div>
<field name="letsencrypt_altnames" />
</div>
<div class="o_setting_right_pane">
<label for="letsencrypt_reload_command"/>
<div class="text-muted">Write a command to reload the server</div>
<field name="letsencrypt_reload_command" />
</div>
<div class="o_setting_right_pane">
<label for="letsencrypt_dns_provider"/>
<div class="text-muted">Set a DNS provider if you need wildcard certificates</div>
<div class="content-group">
<div class="mt16 row">
<label for="letsencrypt_dns_provider"
class="col-xs-3 col-md-3 o_light_label"/>
<field
class="oe_inline"
name="letsencrypt_dns_provider"
attrs="{'required': [('letsencrypt_needs_dns_provider', '=', True)]}" />
</div>
</div>
</div>
<span id="letsencrypt_dns_provider_settings">
<div class="o_setting_right_pane"
attrs="{'invisible': [('letsencrypt_dns_provider', '!=', 'shell')]}">
<label for="letsencrypt_dns_shell_script" />
<div class="text-muted">Write a shell script to update your DNS records</div>
<field name="letsencrypt_dns_shell_script"
attrs="{'required': [('letsencrypt_dns_provider', '=', 'shell')]}" />
</div>
</span>
<div class="o_setting_left_pane">
<field name="letsencrypt_testing_mode" />
</div>
<div class="o_setting_right_pane">
<label for="letsencrypt_testing_mode"/>
<div class="text-muted">Use the testing server, which has higher rate limits but creates invalid certificates.</div>
</div>
<div class="o_setting_left_pane">
<field name="letsencrypt_prefer_dns" />
</div>
<div class="o_setting_right_pane">
<label for="letsencrypt_prefer_dns"/>
<div class="text-muted">Validate through DNS even when HTTP validation is possible. Use this if your Odoo instance isn't publicly accessible.</div>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>