From a0ce33a53788757d16ad7bc4e84f8e7e04f959c7 Mon Sep 17 00:00:00 2001 From: Bombar Maxime Date: Sun, 19 Apr 2020 19:57:38 +0200 Subject: [PATCH 01/11] [re2oapi] If user is not specified, use re2o_service_user from the cpasswords vault. --- lookup_plugins/re2oapi.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lookup_plugins/re2oapi.py b/lookup_plugins/re2oapi.py index 6dcbe2b9..3e047916 100644 --- a/lookup_plugins/re2oapi.py +++ b/lookup_plugins/re2oapi.py @@ -12,8 +12,6 @@ For now: TODO: Implement a small client for our needs, this will also remove the sys.path extension ... """ - - from ansible.plugins.lookup import LookupBase from ansible.errors import AnsibleError @@ -47,6 +45,10 @@ class LookupModule(LookupBase): if api_hostname is None: raise AnsibleError('You must specify a hostname to contact re2oAPI') + if api_username is None and api_password is None: + api_username = variables.get('vault_re2o_service_user') + api_password = variables.get('vault_re2o_service_password') + if api_username is None: raise AnsibleError('You must specify a valid username to connect to re2oAPI') From 23d8252f5f5c1d4e278c1b2decb2b3a31d3c2571 Mon Sep 17 00:00:00 2001 From: Bombar Maxime Date: Sun, 19 Apr 2020 19:57:55 +0200 Subject: [PATCH 02/11] [re2oapi] Improve documentation. --- lookup_plugins/re2oapi.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lookup_plugins/re2oapi.py b/lookup_plugins/re2oapi.py index 3e047916..b4a87fc7 100644 --- a/lookup_plugins/re2oapi.py +++ b/lookup_plugins/re2oapi.py @@ -21,10 +21,20 @@ sys.path.append('./lookup_plugins/') from re2oapi import Re2oAPIClient - class LookupModule(LookupBase): """ - If terms = dnszones then this module queries the re2o api and returns the list of all dns zones + If terms = dnszones then this module queries the re2o api and returns the list of all dns zones. + + + Usage: + + The following play will use the debug module to output all the zone names managed by crans. + + - hosts: sputnik.adm.crans.org + vars: + dnszones: "{{ lookup('re2oapi', 'dnszones', api_hostname='intranet.crans.org') }}" + tasks: + - debug: var=dnszones """ From 15cf56ba655b474367c7912856027896ff49e5f5 Mon Sep 17 00:00:00 2001 From: Bombar Maxime Date: Sat, 25 Apr 2020 23:10:01 +0200 Subject: [PATCH 03/11] Standalone re2o api lookup plugin --- .gitmodules | 3 - lookup_plugins/re2oapi | 1 - lookup_plugins/re2oapi.py | 386 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 367 insertions(+), 23 deletions(-) delete mode 160000 lookup_plugins/re2oapi diff --git a/.gitmodules b/.gitmodules index a5cf29f9..59564548 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "roles/re2o-mail-server/templates/re2o-services/mail-server/mail-aliases"] path = roles/re2o-mail-server/templates/re2o-services/mail-server/mail-aliases url = https://gitlab.crans.org/nounous/mail-aliases -[submodule "re2o-re2oapi"] - path = lookup_plugins/re2oapi - url = git@gitlab.crans.org:nounous/re2o-re2oapi.git diff --git a/lookup_plugins/re2oapi b/lookup_plugins/re2oapi deleted file mode 160000 index 6565b92f..00000000 --- a/lookup_plugins/re2oapi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6565b92f3bfc13d02b95888ae021f5bd6f7ef317 diff --git a/lookup_plugins/re2oapi.py b/lookup_plugins/re2oapi.py index b4a87fc7..6b4bef87 100644 --- a/lookup_plugins/re2oapi.py +++ b/lookup_plugins/re2oapi.py @@ -1,34 +1,368 @@ """ -A Proof Of Concept of lookup plugin to query the re2o API. +A lookup plugin to query the re2o API. For a detailed example look at https://github.com/ansible/ansible/blob/3dbf89e8aeb80eb2d1484b1cb63458e4bb12795a/lib/ansible/plugins/lookup/aws_ssm.py -For now: - - - Need to clone nounous/re2o-re2oapi.git and checkout to crans branch. - - This Re2oAPIClient needs python3-iso8601 - -TODO: Implement a small client for our needs, this will also remove the sys.path extension ... +The API Client has been adapted from https://gitlab.federez.net/re2o/re2oapi """ +from pathlib import Path +import datetime +import requests +import stat +import json + +from ansible.module_utils._text import to_native from ansible.plugins.lookup import LookupBase -from ansible.errors import AnsibleError +from ansible.errors import (AnsibleError, + AnsibleFileNotFound, + AnsibleLookupError, + ) +from ansible.utils.display import Display -import sys -sys.path.append('./lookup_plugins/') +# Ansible Logger to stdout +display = Display() -from re2oapi import Re2oAPIClient +# Number of seconds before expiration where renewing the token is done +TIME_FOR_RENEW = 120 +# Default name of the file to store tokens. Path $HOME/{DEFAUlt_TOKEN_FILENAME} +DEFAULT_TOKEN_FILENAME = '.re2o.token' + + +class Client: + """ + Class based client to contact re2o API. + """ + def __init__(self, hostname, username, password, use_tls=True): + """ + :arg hostname: The hostname of the Re2o instance to use. + :arg username: The username to use. + :arg password: The password to use. + :arg use_tls: A boolean to specify whether the client should use a + a TLS connection. Default is True. Please, keep it. + """ + self.use_tls = use_tls + self.hostname = hostname + self._username = username + self._password = password + + self.token_file = Path.home() / DEFAULT_TOKEN_FILENAME + + display.v("Connecting to {hostname} as user {user}".format( + hostname=to_native(self.hostname), user=to_native(self._username))) + try: + self.token = self._get_token_from_file() + except AnsibleFileNotFound: + display.vv("Force renew the token") + self._force_renew_token() + + def _get_token_from_file(self): + display.vv("Trying to fetch token from {}".format(self.token_file)) + + # Check if the token file exists + if not self.token_file.is_file(): + display.vv("Unable to access file {}".format(self.token_file)) + raise AnsibleFileNotFound(file_name=self.token_file) + + try: + with self.token_file.open() as f: + data = json.load(f) + except Exception as e: + display.vv("File {} not readable".format(self.token_file)) + display.vvv("Original error was {}".format(to_native(e))) + raise AnsibleFileNotFound(file_name=self.token_file.as_posix() + + ' (Not readable)') + + try: + token_data = data[self.hostname][self._username] + ret = { + 'token': token_data['token'], + 'expiration': self._parse_date(token_data["expiration"]), + } + + except KeyError: + raise AnsibleLookupError("""Token for {user}@{host} not found + in token file ({token})""".format(user=self._username, + host=self.hostname, + token=self.token_file, + ) + ) + else: + display.vv("""Token successfully retreived from + file {token}""".format(token=self.token_file)) + return ret + + def _force_renew_token(self): + self.token = self._get_token_from_server() + self._save_token_to_file() + + def _get_token_from_server(self): + display.vv("Requesting a new token for {user}@{host}".format( + user=self._username, + host=self.hostname, + )) + # Authentication request + response = requests.post( + self.get_url_for('token-auth'), + data={'username': self._username, 'password': self._password}, + ) + display.vv("Response code: {}".format(response.status_code)) + if response.status_code == requests.codes.bad_request: + display.vv("Please provide valid credentials") + raise AnsibleLookupError("Unable to connect to the API for {host}" + .format(host=self.hostname)) + try: + response.raise_for_status() + except Exception as e: + raise AnsibleError("""An error occured while trying to contact + the API. This was the original exception: {}""" + .format(to_native(e))) + + response = response.json() + ret = { + 'token': response['token'], + 'expiration': self._parse_date(response['expiration']), + } + + display.vv("Token successfully retreived for {user}@{host}".format( + user=self._username, + host=self.hostname, + ) + ) + return ret + + def _parse_date(self, date, date_format="%Y-%m-%dT%H:%M:%S"): + return datetime.datetime.strptime(date.split('.')[0], date_format) + + def _save_token_to_file(self): + display.vv("Saving token to file {}".format(self.token_file)) + try: + # Read previous data to avoid erasures + with self.token_file.open() as f: + data = json.load(f) + except Exception: + display.v("""Beware, token file {} was not a valid JSON readable + file. Considered empty.""".format(self.token_file)) + data = {} + + if self.hostname not in data.keys(): + data[self.hostname] = {} + data[self.hostname][self._username] = { + 'token': self.token['token'], + 'expiration': self.token['expiration'].isoformat(), + } + + try: + with self.token_file.open('w') as f: + json.dump(data, f) + self.token_file.chmod(stat.S_IWRITE | stat.S_IREAD) + except Exception as e: + display.vv("Token file {} could not be written. Passing." + .format(self.token_file)) + display.vvv("Original error was {}".format(to_native(e))) + else: + display.vv("Token successfully written to file {}" + .format(self.token_file)) + + def get_token(self): + """ + Retrieves the token to use for the current connection. + Automatically renewed if needed. + """ + if self.need_renew_token: + self._force_renew_token() + + return self.token['token'] + + @property + def need_renew_token(self): + return self.token['expiration'] < \ + datetime.datetime.now() + \ + datetime.timedelta(seconds=TIME_FOR_RENEW) + + def _request(self, method, url, headers={}, params={}, *args, **kwargs): + display.vv("Building the {method} request to {url}.".format( + method=method.upper(), + url=url, + )) + + # Force the 'Authorization' field with the right token. + display.vvv("Forcing authentication token.") + headers.update({ + 'Authorization': 'Token {}'.format(self.get_token()) + }) + + # Use a json format unless the user already specified something + if 'format' not in params.keys(): + display.vvv("Forcing JSON format response.") + params.update({'format': 'json'}) + + # Perform the request + display.v("{} {}".format(method.upper(), url)) + response = getattr(requests, method)( + url, headers=headers, params=params, *args, **kwargs + ) + display.vvv("Response code: {}".format(response.status_code)) + + if response.status_code == requests.codes.unauthorized: + # Force re-login to the server (case of a wrong token but valid + # credentials) and then retry the request without catching errors. + display.vv("Token refused. Trying to refresh the token.") + self._force_renew_token() + + headers.update({ + 'Authorization': 'Token {}'.format(self.get_token()) + }) + display.vv("Re-performing the request {method} {url}".format( + method=method.upper(), + url=url, + )) + response = getattr(requests, method)( + url, headers=headers, params=params, *args, **kwargs + ) + display.vvv("Response code: ".format(response.status_code)) + + if response.status_code == requests.codes.forbidden: + err = "The {method} request to {url} was denied for {user}".format( + method=method.upper(), + url=url, + user=self._username + ) + display.vvv(err) + raise AnsibleLookupError(to_native(err)) + + try: + response.raise_for_status() + except Exception as e: + raise AnsibleError("""An error occured while trying to contact + the API. This was the original exception: {}""" + .format(to_native(e))) + + ret = response.json() + display.vvv("{method} request to {url} successful.".format( + method=method.upper(), + url=url + )) + return ret + + def get_url_for(self, endpoint): + """ + Retrieves the complete URL to use for a given endpoint's name. + """ + return '{proto}://{host}/{namespace}/{endpoint}'.format( + proto=('https' if self.use_tls else 'http'), + host=self.hostname, + namespace='api', + endpoint=endpoint + ) + + def get(self, *args, **kwargs): + """ + Perform a GET request to the API + """ + return self._request('get', *args, **kwargs) + + def list(self, endpoint, max_results=None, params={}): + """List all objects on the server that corresponds to the given + endpoint. The endpoint must be valid for listing objects. + + :arg endpoint: The path of the endpoint. + :kwarg max_results: A limit on the number of result to return + :kwarg params: See `requests.get` params. + :returns: The list of all the objects as returned by the API. + """ + display.v("Starting listing objects under '{}'" + .format(endpoint)) + display.vvv("max_results = {}".format(max_results)) + + # For optimization, list all results in one page unless the user + # is forcing a different `page_size`. + if 'page_size' not in params.keys(): + display.vvv("Forcing 'page_size' parameter to 'all'.") + params['page_size'] = max_results or 'all' + + # Performs the request for the first page + response = self.get( + self.get_url_for(endpoint), + params=params, + ) + + results = response['results'] + + # Get all next pages and append the results + while response['next'] is not None and \ + (max_results is None or len(results) < max_results): + response = self.get(response['next']) + results += response['results'] + + # Returns the exact number of results if applicable + ret = results[:max_results] if max_results else results + display.vvv("Listing objects under '{}' successful" + .format(endpoint)) + return ret + + def count(self, endpoint, params={}): + """Counts all objects on the server that corresponds to the given + endpoint. The endpoint must be valid for listing objects. + + :arg endpoint: The path of the endpoint. + :kwarg params: See `requests.get` params. + :returns: Number of objects on the server as returned by the API. + """ + display.v("Starting counting objects under '{}'" + .format(endpoint)) + + # For optimization, ask for only 1 result (so the server will take + # less time to process the request) unless the user is forcing + # a different `page_size`. + if 'page_size' not in params.keys(): + display.vvv("Forcing 'page_size' parameter to '1'.") + params['page_size'] = 1 + + # Performs the request and return the `count` value in the response. + ret = self.get( + self.get_url_for(endpoint), + params=params, + )['count'] + + display.vvv("Counting objects under '{}' successful" + .format(endpoint)) + return ret + + def view(self, endpoint, params={}): + """Retrieves the details of an object from the server that corresponds + to the given endpoint. + + :args endpoint: The path of the endpoint. + :kwargs params: See `requests.get` params. + :returns: The object serialized as returned by the API. + """ + display.v("Starting viewing an object under '{}'" + .format(endpoint)) + ret = self.get( + self.get_url_for(endpoint), + params=params + ) + + display.vvv("Viewing object under '{}' successful" + .format(endpoint)) + return ret class LookupModule(LookupBase): """ - If terms = dnszones then this module queries the re2o api and returns the list of all dns zones. + Available terms = + - dnszones: Queries the re2o API and returns the list of all dns zones + nicely formatted to be rendered in a template. + If a term is not in the previous list, make a raw query to the API + with endpoint term. Usage: - The following play will use the debug module to output all the zone names managed by crans. + The following play will use the debug module to output + all the zone names managed by Crans. - hosts: sputnik.adm.crans.org vars: @@ -37,7 +371,6 @@ class LookupModule(LookupBase): - debug: var=dnszones """ - def run(self, terms, variables=None, api_hostname=None, api_username=None, api_password=None, use_tls=True): @@ -48,32 +381,47 @@ class LookupModule(LookupBase): :kwarg api_hostname: The hostname of re2o instance. :kwarg api_username: The username to connect to the API. :kwarg api_password: The password to use to connect to the API. - :kwarg use_tls: A boolean to specify whether to use tls or not. You should ! + :kwarg use_tls: A boolean to specify whether to use tls. You should! :returns: A list of results to the specific queries. """ if api_hostname is None: - raise AnsibleError('You must specify a hostname to contact re2oAPI') + raise AnsibleError(to_native( + 'You must specify a hostname to contact re2oAPI' + )) if api_username is None and api_password is None: api_username = variables.get('vault_re2o_service_user') api_password = variables.get('vault_re2o_service_password') if api_username is None: - raise AnsibleError('You must specify a valid username to connect to re2oAPI') + raise AnsibleError(to_native( + 'You must specify a valid username to connect to re2oAPI' + )) if api_password is None: - raise AnsibleError('You must specify a valid password to connect to re2oAPI') + raise AnsibleError(to_native( + 'You must specify a valid password to connect to re2oAPI' + )) - api_client = Re2oAPIClient(api_hostname, api_username, api_password, use_tls=True) + api_client = Client(api_hostname, api_username, + api_password, use_tls=True) res = [] for term in terms: + display.v("\nLookup for {} \n".format(term)) if term == 'dnszones': res.append(self._getzones(api_client)) + else: + res.append(self._rawquery(api_client, term)) return res def _getzones(self, api_client): + display.v("Getting dns zone names") zones = api_client.list('dns/zones') zones_name = [zone["name"][1:] for zone in zones] return zones_name + + def _rawquery(self, api_client, endpoint): + display.v("Make a raw query to endpoint {}".format(endpoint)) + return api_client.list(endpoint) From ce87098727a776f79ef0e9862d8b484593455cd9 Mon Sep 17 00:00:00 2001 From: Bombar Maxime Date: Sat, 25 Apr 2020 23:14:33 +0200 Subject: [PATCH 04/11] [re2o_lookup] Ansible way to manage errors. --- lookup_plugins/re2oapi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lookup_plugins/re2oapi.py b/lookup_plugins/re2oapi.py index 6b4bef87..52a52a34 100644 --- a/lookup_plugins/re2oapi.py +++ b/lookup_plugins/re2oapi.py @@ -413,7 +413,13 @@ class LookupModule(LookupBase): if term == 'dnszones': res.append(self._getzones(api_client)) else: - res.append(self._rawquery(api_client, term)) + try: + res.append(self._rawquery(api_client, term)) + except Exception as e: + raise AnsibleError(""" + An error occured while running re2oapi + lookup plugin. Original message was : {}""" + .format(to_native(e))) return res def _getzones(self, api_client): From ca5646a52bc841e0b62850998285df55f019b489 Mon Sep 17 00:00:00 2001 From: Bombar Maxime Date: Sat, 25 Apr 2020 23:23:34 +0200 Subject: [PATCH 05/11] [re2o_lookup] Remove useless methods --- lookup_plugins/re2oapi.py | 47 --------------------------------------- 1 file changed, 47 deletions(-) diff --git a/lookup_plugins/re2oapi.py b/lookup_plugins/re2oapi.py index 52a52a34..06dffb53 100644 --- a/lookup_plugins/re2oapi.py +++ b/lookup_plugins/re2oapi.py @@ -302,53 +302,6 @@ class Client: .format(endpoint)) return ret - def count(self, endpoint, params={}): - """Counts all objects on the server that corresponds to the given - endpoint. The endpoint must be valid for listing objects. - - :arg endpoint: The path of the endpoint. - :kwarg params: See `requests.get` params. - :returns: Number of objects on the server as returned by the API. - """ - display.v("Starting counting objects under '{}'" - .format(endpoint)) - - # For optimization, ask for only 1 result (so the server will take - # less time to process the request) unless the user is forcing - # a different `page_size`. - if 'page_size' not in params.keys(): - display.vvv("Forcing 'page_size' parameter to '1'.") - params['page_size'] = 1 - - # Performs the request and return the `count` value in the response. - ret = self.get( - self.get_url_for(endpoint), - params=params, - )['count'] - - display.vvv("Counting objects under '{}' successful" - .format(endpoint)) - return ret - - def view(self, endpoint, params={}): - """Retrieves the details of an object from the server that corresponds - to the given endpoint. - - :args endpoint: The path of the endpoint. - :kwargs params: See `requests.get` params. - :returns: The object serialized as returned by the API. - """ - display.v("Starting viewing an object under '{}'" - .format(endpoint)) - ret = self.get( - self.get_url_for(endpoint), - params=params - ) - - display.vvv("Viewing object under '{}' successful" - .format(endpoint)) - return ret - class LookupModule(LookupBase): """ From b7ec18418896d787202295b9850a90006c326e60 Mon Sep 17 00:00:00 2001 From: Benjamin Graillot Date: Mon, 20 Apr 2020 08:32:47 +0200 Subject: [PATCH 06/11] [quagga] Fix comments in bgpd and zebra config --- roles/quagga-ipv4/templates/quagga/bgpd.conf.j2 | 2 +- roles/quagga-ipv4/templates/quagga/zebra.conf.j2 | 2 +- roles/quagga-ipv6/templates/quagga/bgpd.conf.j2 | 2 +- roles/quagga-ipv6/templates/quagga/zebra.conf.j2 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/quagga-ipv4/templates/quagga/bgpd.conf.j2 b/roles/quagga-ipv4/templates/quagga/bgpd.conf.j2 index f3829097..d87269e3 100644 --- a/roles/quagga-ipv4/templates/quagga/bgpd.conf.j2 +++ b/roles/quagga-ipv4/templates/quagga/bgpd.conf.j2 @@ -1,4 +1,4 @@ -! {{ ansible_header | comment }} +{{ ansible_header | comment(decoration='! ') }} router bgp {{ bgp.as }} no synchronization diff --git a/roles/quagga-ipv4/templates/quagga/zebra.conf.j2 b/roles/quagga-ipv4/templates/quagga/zebra.conf.j2 index 4f3b6367..1f3a31ca 100644 --- a/roles/quagga-ipv4/templates/quagga/zebra.conf.j2 +++ b/roles/quagga-ipv4/templates/quagga/zebra.conf.j2 @@ -1,4 +1,4 @@ -! {{ ansible_header | comment }} +{{ ansible_header | comment(decoration='! ') }} hostname zebra password {{ zebra.password }} diff --git a/roles/quagga-ipv6/templates/quagga/bgpd.conf.j2 b/roles/quagga-ipv6/templates/quagga/bgpd.conf.j2 index 2fe4a763..5021cade 100644 --- a/roles/quagga-ipv6/templates/quagga/bgpd.conf.j2 +++ b/roles/quagga-ipv6/templates/quagga/bgpd.conf.j2 @@ -1,4 +1,4 @@ -! {{ ansible_header | comment }} +{{ ansible_header | comment(decoration='! ') }} router bgp {{ bgp.as }} no synchronization diff --git a/roles/quagga-ipv6/templates/quagga/zebra.conf.j2 b/roles/quagga-ipv6/templates/quagga/zebra.conf.j2 index aa63898a..1db5e12d 100644 --- a/roles/quagga-ipv6/templates/quagga/zebra.conf.j2 +++ b/roles/quagga-ipv6/templates/quagga/zebra.conf.j2 @@ -1,4 +1,4 @@ -! {{ ansible_header | comment }} +{{ ansible_header | comment(decoration='! ') }} hostname zebra password {{ zebra.password }} From 4c660152b563a36ba2a6c5aabf1f307670ed59fc Mon Sep 17 00:00:00 2001 From: Benjamin Graillot Date: Mon, 20 Apr 2020 08:42:55 +0200 Subject: [PATCH 07/11] [re2o-mail-server] Update mail-aliases submodule --- .../templates/re2o-services/mail-server/mail-aliases | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/re2o-mail-server/templates/re2o-services/mail-server/mail-aliases b/roles/re2o-mail-server/templates/re2o-services/mail-server/mail-aliases index 3d365dae..3fa31a21 160000 --- a/roles/re2o-mail-server/templates/re2o-services/mail-server/mail-aliases +++ b/roles/re2o-mail-server/templates/re2o-services/mail-server/mail-aliases @@ -1 +1 @@ -Subproject commit 3d365dae2c8b3c0b2e02e8d4b134a7b6796bf99b +Subproject commit 3fa31a218d75835aa196ebd174906f3656ef22bd From 3ba546a3091a3cfd509f2cc133b5bc467f9bf55f Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sat, 25 Apr 2020 18:44:25 +0200 Subject: [PATCH 08/11] [bind authoritative] Revert to debian conf --- .../templates/bind/named.conf.local.j2 | 4 ++ .../templates/bind/named.conf.options.j2 | 66 +++++-------------- 2 files changed, 21 insertions(+), 49 deletions(-) diff --git a/roles/bind-authoritative/templates/bind/named.conf.local.j2 b/roles/bind-authoritative/templates/bind/named.conf.local.j2 index 1b002267..09653cd1 100644 --- a/roles/bind-authoritative/templates/bind/named.conf.local.j2 +++ b/roles/bind-authoritative/templates/bind/named.conf.local.j2 @@ -1,5 +1,9 @@ {{ ansible_header | comment(decoration='// ') }} +// Consider adding the 1918 zones here, if they are not used in your +// organization +//include "/etc/bind/zones.rfc1918"; + {% if not bind.master %} {% for zone in bind.zones %} zone "{{ zone }}" { diff --git a/roles/bind-authoritative/templates/bind/named.conf.options.j2 b/roles/bind-authoritative/templates/bind/named.conf.options.j2 index 7138794d..1b0c09ac 100644 --- a/roles/bind-authoritative/templates/bind/named.conf.options.j2 +++ b/roles/bind-authoritative/templates/bind/named.conf.options.j2 @@ -1,58 +1,26 @@ {{ ansible_header | comment(decoration='// ') }} -// Listes d'acces -acl "isolement" { 10.52.0.0/16; }; -acl "accueil" { 10.51.0.0/16; }; -acl "switches" { 10.231.100.0/24; }; -acl "event" { 10.231.137.0/24; 2a0c:700:0:10::/64; }; -acl "fil-new" { 10.54.1.0/24; 10.54.2.0/23; 10.54.4.0/22; 10.54.8.0/21; 10.54.16.0/21; 10.54.24.0/23; 10.54.0.0/24; 2a0c:700:0:21::/64; }; -acl "wifi-new" { 10.53.1.0/24; 10.53.2.0/23; 10.53.4.0/22; 10.53.8.0/21; 10.53.16.0/22; 10.53.20.0/24; 10.53.0.0/24; 10.53.21.0/24; 10.53.22.0/23; 10.53.24.0/23; 2a0c:700:0:22::/64; }; -acl "crans" { 2a0c:700:0:1::/64; 138.231.137.0/24; 138.231.138.0/23; 138.231.140.0/22; 185.230.77.0/24; 2a0c:700:0:21::/64; 2a0c:700:0:23::/64; 185.230.78.0/24; 185.230.76.0/24; 2a0c:700:0:22::/64; 138.231.136.0/24; }; -acl "cransadm" { 2a0c:700:0:2::/64; 10.231.136.0/24; }; -acl "bornes" { fd01:240:fe3d:3::/64; 10.231.148.0/24; }; options { - directory "/var/cache/bind"; + directory "/var/cache/bind"; - // If there is a firewall between you and nameservers you want - // to talk to, you may need to fix the firewall to allow multiple - // ports to talk. See http://www.kb.cert.org/vuls/id/800113 + // If there is a firewall between you and nameservers you want + // to talk to, you may need to fix the firewall to allow multiple + // ports to talk. See http://www.kb.cert.org/vuls/id/800113 - // If your ISP provided one or more IP addresses for stable - // nameservers, you probably want to use them as forwarders. - // Uncomment the following block, and insert the addresses replacing - // the all-0's placeholder. + // If your ISP provided one or more IP addresses for stable + // nameservers, you probably want to use them as forwarders. + // Uncomment the following block, and insert the addresses replacing + // the all-0's placeholder. - // forwarders { - // 0.0.0.0; - // }; + // forwarders { + // 0.0.0.0; + // }; - //======================================================================== - // If BIND logs error messages about the root key being expired, - // you will need to update your keys. See https://www.isc.org/bind-keys - //======================================================================== + //======================================================================== + // If BIND logs error messages about the root key being expired, + // you will need to update your keys. See https://www.isc.org/bind-keys + //======================================================================== + dnssec-validation auto; - - allow-query-cache { 127.0.0.1; crans; cransadm; bornes; }; - allow-recursion { 127.0.0.1; crans; cransadm; bornes; }; - notify no; - allow-transfer { "none"; }; - recursive-clients 5000; - allow-query { any; }; - auth-nxdomain no; # conform to RFC1035 - - listen-on { any; }; - listen-on-v6 { any; }; - - dnssec-enable no; - dnssec-validation no; -}; -logging{ - // Remove "REFUSED unexpected RCODE resolving" from the logfile - category lame-servers { null; }; -}; -// to allow for rndc flush -include "/etc/bind/rndc.key"; - -controls { - inet 127.0.0.1 allow { 127.0.0.1; } keys { "key"; }; + listen-on-v6 { any; }; }; From 424e0df45e24f222b475a6a257e210a19d50ca1c Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sat, 25 Apr 2020 20:22:26 +0200 Subject: [PATCH 09/11] [bind] Add master configuration --- network.yml | 8 ++--- roles/bind-authoritative/handlers/main.yml | 5 +++ roles/bind-authoritative/tasks/main.yml | 8 ++--- .../templates/bind/named.conf.local.j2 | 34 +++++++++++++++++-- 4 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 roles/bind-authoritative/handlers/main.yml diff --git a/network.yml b/network.yml index 5a28e7d0..f3f8c589 100755 --- a/network.yml +++ b/network.yml @@ -39,14 +39,12 @@ # Deplay authoritative DNS server - hosts: sputnik.adm.crans.org vars: + certbot_dns_secret: "{{ vault_certbot_dns_secret }}" bind: master: false master_ip: 10.231.136.118 - dnssec: false - zones: - - crans.org - - crans.eu - - crans.fr + slaves: [] # TODO + zones: "{{ lookup('re2oapi', 'dnszones', api_hostname='intranet.crans.org') }}" roles: - bind-authoritative diff --git a/roles/bind-authoritative/handlers/main.yml b/roles/bind-authoritative/handlers/main.yml new file mode 100644 index 00000000..0f5025c5 --- /dev/null +++ b/roles/bind-authoritative/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Reload Bind9 + systemd: + name: bind9 + state: reloaded diff --git a/roles/bind-authoritative/tasks/main.yml b/roles/bind-authoritative/tasks/main.yml index 407e533e..47d16b22 100644 --- a/roles/bind-authoritative/tasks/main.yml +++ b/roles/bind-authoritative/tasks/main.yml @@ -11,15 +11,11 @@ template: src: bind/{{ item }}.j2 dest: /etc/bind/{{ item }} - mode: 0644 + mode: 0640 owner: root group: bind loop: - named.conf - named.conf.local - named.conf.options - -- name: Reload Bind9 - systemd: - name: bind9 - state: reloaded + notify: Reload Bind9 diff --git a/roles/bind-authoritative/templates/bind/named.conf.local.j2 b/roles/bind-authoritative/templates/bind/named.conf.local.j2 index 09653cd1..0f7603c1 100644 --- a/roles/bind-authoritative/templates/bind/named.conf.local.j2 +++ b/roles/bind-authoritative/templates/bind/named.conf.local.j2 @@ -4,15 +4,43 @@ // organization //include "/etc/bind/zones.rfc1918"; -{% if not bind.master %} +{% if bind.master %} +// Let's Encrypt Challenge DNS-01 +key "certbot_challenge." { + algorithm hmac-sha512; + secret "{{ certbot_dns_secret }}"; +}; +{% endif %} + +// Crans zones {% for zone in bind.zones %} zone "{{ zone }}" { + {% if bind.master %} + type master; + file "/var/local/re2o-services/dns/generated/dns.{{ zone }}.zone"; + forwarders { + {% for slave in bind.slaves -%} + {{ slave }}; + {% endfor -%} + }; + allow-transfer { + {% for slave in bind.slaves -%} + {{ slave }}; + {% endfor -%} + }; + update-policy { + grant certbot_challenge. name _acme-challenge.{{ zone }} txt; + }; + notify yes; + {% else %} type slave; - masters { {{ bind.master_ip }}; }; file "bak.{{ zone }}"; + masters { + {{ bind.master_ip }}; + }; allow-transfer { "none"; }; notify no; + {% endif %} }; {% endfor %} -{% endif %} From 2dbe3c50b9d608cd2365a1dd85753213da874630 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sat, 25 Apr 2020 20:57:32 +0200 Subject: [PATCH 10/11] Initial certbot conf --- .../templates/bind/named.conf.j2 | 3 --- roles/certbot/tasks/main.yml | 25 ++++++++++++++++++ .../letsencrypt/conf.d/wildcard.ini.j2 | 26 +++++++++++++++++++ .../templates/letsencrypt/rfc2136.ini.j2 | 7 +++++ 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 roles/certbot/tasks/main.yml create mode 100644 roles/certbot/templates/letsencrypt/conf.d/wildcard.ini.j2 create mode 100644 roles/certbot/templates/letsencrypt/rfc2136.ini.j2 diff --git a/roles/bind-authoritative/templates/bind/named.conf.j2 b/roles/bind-authoritative/templates/bind/named.conf.j2 index fdac65db..c6bfaee4 100644 --- a/roles/bind-authoritative/templates/bind/named.conf.j2 +++ b/roles/bind-authoritative/templates/bind/named.conf.j2 @@ -8,9 +8,6 @@ // // If you are just adding zones, please do that in /etc/bind/named.conf.local -{% if bind.dnssec %} -include "/etc/bind/bind.keys"; -{% endif %} include "/etc/bind/named.conf.options"; include "/etc/bind/named.conf.local"; include "/etc/bind/named.conf.default-zones"; diff --git a/roles/certbot/tasks/main.yml b/roles/certbot/tasks/main.yml new file mode 100644 index 00000000..25c1c88b --- /dev/null +++ b/roles/certbot/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: Install certbot and RFC2136 plugin + apt: + update_cache: true + name: + - certbot + - python3-certbot-dns-rfc2136 + state: present + register: apt_result + retries: 3 + until: apt_result is succeeded + +- name: Add DNS credentials + template: + src: letsencrypt/rfc2136.ini.j2 + dest: /etc/letsencrypt/rfc2136.ini + mode: 0600 + user: root + +- name: Add Certbot configuration + template: + src: letsencrypt/conf.d/wildcard.ini.j2 + dest: /etc/letsencrypt/conf.d/wildcard.ini + mode: 0644 + diff --git a/roles/certbot/templates/letsencrypt/conf.d/wildcard.ini.j2 b/roles/certbot/templates/letsencrypt/conf.d/wildcard.ini.j2 new file mode 100644 index 00000000..394632f6 --- /dev/null +++ b/roles/certbot/templates/letsencrypt/conf.d/wildcard.ini.j2 @@ -0,0 +1,26 @@ +{{ ansible_header | comment(decoration='# ') }} + +# Pour appliquer cette conf et générer la conf de renewal : +# certbot --config wildcard.ini certonly + +# Use a 4096 bit RSA key instead of 2048 +rsa-key-size = 4096 + +# Always use the staging/testing server +# server = https://acme-staging.api.letsencrypt.org/directory +# server = https://acme-v01.api.letsencrypt.org/directory + +# Uncomment and update to register with the specified e-mail address +email = root@crans.org + +# Uncomment to use a text interface instead of ncurses +text = True + +# Use DNS-01 challenge +authenticator = dns-rfc2136 +dns-rfc2136-credentials = /etc/letsencrypt/rfc2136.ini +dns-rfc2136-propagation-seconds = 30 + +# Wildcard the domain +cert-name = crans.org +domains = crans.org, *.crans.org diff --git a/roles/certbot/templates/letsencrypt/rfc2136.ini.j2 b/roles/certbot/templates/letsencrypt/rfc2136.ini.j2 new file mode 100644 index 00000000..80d3dde6 --- /dev/null +++ b/roles/certbot/templates/letsencrypt/rfc2136.ini.j2 @@ -0,0 +1,7 @@ +{{ ansible_header | comment(decoration='# ') }} + +dns_rfc2136_server = {{ dns_master }} +dns_rfc2136_port = 53 +dns_rfc2136_name = certbot_challenge. +dns_rfc2136_secret = {{ certbot_dns_secret }} +dns_rfc2136_algorithm = HMAC-SHA512 From a44eb7ea5421050b23b60cf00c7d7f5726ba1590 Mon Sep 17 00:00:00 2001 From: Bombar Maxime Date: Sun, 26 Apr 2020 14:49:29 +0200 Subject: [PATCH 11/11] [re2o_lookup] Adapt get_role from bcfg2/ip.py --- lookup_plugins/re2oapi.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lookup_plugins/re2oapi.py b/lookup_plugins/re2oapi.py index 06dffb53..a8b64db6 100644 --- a/lookup_plugins/re2oapi.py +++ b/lookup_plugins/re2oapi.py @@ -12,6 +12,7 @@ import datetime import requests import stat import json +import collections from ansible.module_utils._text import to_native from ansible.plugins.lookup import LookupBase @@ -309,6 +310,10 @@ class LookupModule(LookupBase): - dnszones: Queries the re2o API and returns the list of all dns zones nicely formatted to be rendered in a template. + - get_role, role_name: Works in pair. Fails if role_name not provided. + Queries the re2o API and returns the list of + all machines whose role_type is role_name. + If a term is not in the previous list, make a raw query to the API with endpoint term. @@ -361,10 +366,25 @@ class LookupModule(LookupBase): api_password, use_tls=True) res = [] - for term in terms: + dterms = collections.deque(terms) + machines_roles = None # TODO : Cache this. + display.vvv("Lookup terms are {}".format(terms)) + while dterms: + term = dterms.popleft() display.v("\nLookup for {} \n".format(term)) if term == 'dnszones': res.append(self._getzones(api_client)) + elif term == 'get_role': + try: + role_name = dterms.popleft() + roles, machines_roles = self._get_role(api_client, + role_name, + machines_roles, + ) + res.append(roles) + except IndexError: + display.v("Error in re2oapi : No role_name provided") + raise AnsibleError("role_name not found in arguments.") else: try: res.append(self._rawquery(api_client, term)) @@ -384,3 +404,9 @@ class LookupModule(LookupBase): def _rawquery(self, api_client, endpoint): display.v("Make a raw query to endpoint {}".format(endpoint)) return api_client.list(endpoint) + + def _get_role(self, api_client, role_name, machines_roles): + if machines_roles is None: + machines_roles = api_client.list("machines/role") + return list(filter(lambda machine: machine["role_type"] == role_name, + machines_roles)), machines_roles