We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Certbot-dns-google fails with HTTP 412 - Precondition not met
## My operating system is (include version):
CentOS 7

## I installed Certbot with (certbot-auto, OS package manager, pip, etc):
yum install -y certbot
pip install certbot-dns-google

## I ran this command and it produced this output:

certbot certonly --dns-google --dns-google-credentials <path> --server https://acme-v02.api.letsencrypt.org/directory -d ssl1234.example.net -d *.example.net -d example.net  ...

## Certbot's behavior differed from what I expected because:

Encountered error adding TXT record: <HttpError 412 when requesting https://www.googleapis.com/dns/v1/projects/projectname/managedZones/id/changes?alt=json returned "Precondition not met for 'entity.change.deletions[0]'">

This problem only appeared if multiple domains are requested (around 15 SAN Names), single Domains work fine. I tested both wildcard domains and single domains - both fail sometimes. 

## Here is a Certbot log showing the issue (if available):
###### Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.

2018-05-24 16:05:21,163:WARNING:certbot_dns_google.dns_google:Encountered error deleting TXT record: <HttpError 412 when requesting https://www.googleapis.com/dns/v1/projects/projectname/managedZones/id/changes?alt=json returned "Precondition not met for 'entity.change.deletions[0]'">


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `certbot-dns-google/certbot_dns_google/_internal/dns_google.py`
Content:
```
1 """DNS Authenticator for Google Cloud DNS."""
2 import json
3 import logging
4 
5 from googleapiclient import discovery
6 from googleapiclient import errors as googleapiclient_errors
7 import httplib2
8 from oauth2client.service_account import ServiceAccountCredentials
9 import zope.interface
10 
11 from certbot import errors
12 from certbot import interfaces
13 from certbot.plugins import dns_common
14 
15 logger = logging.getLogger(__name__)
16 
17 ACCT_URL = 'https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount'
18 PERMISSIONS_URL = 'https://cloud.google.com/dns/access-control#permissions_and_roles'
19 METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/'
20 METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
21 
22 
23 @zope.interface.implementer(interfaces.IAuthenticator)
24 @zope.interface.provider(interfaces.IPluginFactory)
25 class Authenticator(dns_common.DNSAuthenticator):
26     """DNS Authenticator for Google Cloud DNS
27 
28     This Authenticator uses the Google Cloud DNS API to fulfill a dns-01 challenge.
29     """
30 
31     description = ('Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS '
32                    'for DNS).')
33     ttl = 60
34 
35     def __init__(self, *args, **kwargs):
36         super(Authenticator, self).__init__(*args, **kwargs)
37         self.credentials = None
38 
39     @classmethod
40     def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ
41         super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60)
42         add('credentials',
43             help=('Path to Google Cloud DNS service account JSON file. (See {0} for' +
44                   'information about creating a service account and {1} for information about the' +
45                   'required permissions.)').format(ACCT_URL, PERMISSIONS_URL),
46             default=None)
47 
48     def more_info(self): # pylint: disable=missing-function-docstring
49         return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
50                'the Google Cloud DNS API.'
51 
52     def _setup_credentials(self):
53         if self.conf('credentials') is None:
54             try:
55                 # use project_id query to check for availability of google metadata server
56                 # we won't use the result but know we're not on GCP when an exception is thrown
57                 _GoogleClient.get_project_id()
58             except (ValueError, httplib2.ServerNotFoundError):
59                 raise errors.PluginError('Unable to get Google Cloud Metadata and no credentials'
60                                          ' specified. Automatic credential lookup is only '
61                                          'available on Google Cloud Platform. Please configure'
62                                          ' credentials using --dns-google-credentials <file>')
63         else:
64             self._configure_file('credentials',
65                                  'path to Google Cloud DNS service account JSON file')
66 
67             dns_common.validate_file_permissions(self.conf('credentials'))
68 
69     def _perform(self, domain, validation_name, validation):
70         self._get_google_client().add_txt_record(domain, validation_name, validation, self.ttl)
71 
72     def _cleanup(self, domain, validation_name, validation):
73         self._get_google_client().del_txt_record(domain, validation_name, validation, self.ttl)
74 
75     def _get_google_client(self):
76         return _GoogleClient(self.conf('credentials'))
77 
78 
79 class _GoogleClient(object):
80     """
81     Encapsulates all communication with the Google Cloud DNS API.
82     """
83 
84     def __init__(self, account_json=None, dns_api=None):
85 
86         scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']
87         if account_json is not None:
88             try:
89                 credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)
90                 with open(account_json) as account:
91                     self.project_id = json.load(account)['project_id']
92             except Exception as e:
93                 raise errors.PluginError(
94                     "Error parsing credentials file '{}': {}".format(account_json, e))
95         else:
96             credentials = None
97             self.project_id = self.get_project_id()
98 
99         if not dns_api:
100             self.dns = discovery.build('dns', 'v1',
101                                        credentials=credentials,
102                                        cache_discovery=False)
103         else:
104             self.dns = dns_api
105 
106     def add_txt_record(self, domain, record_name, record_content, record_ttl):
107         """
108         Add a TXT record using the supplied information.
109 
110         :param str domain: The domain to use to look up the managed zone.
111         :param str record_name: The record name (typically beginning with '_acme-challenge.').
112         :param str record_content: The record content (typically the challenge validation).
113         :param int record_ttl: The record TTL (number of seconds that the record may be cached).
114         :raises certbot.errors.PluginError: if an error occurs communicating with the Google API
115         """
116 
117         zone_id = self._find_managed_zone_id(domain)
118 
119         record_contents = self.get_existing_txt_rrset(zone_id, record_name)
120         if record_contents is None:
121             record_contents = []
122         add_records = record_contents[:]
123 
124         if "\""+record_content+"\"" in record_contents:
125             # The process was interrupted previously and validation token exists
126             return
127 
128         add_records.append(record_content)
129 
130         data = {
131             "kind": "dns#change",
132             "additions": [
133                 {
134                     "kind": "dns#resourceRecordSet",
135                     "type": "TXT",
136                     "name": record_name + ".",
137                     "rrdatas": add_records,
138                     "ttl": record_ttl,
139                 },
140             ],
141         }
142 
143         if record_contents:
144             # We need to remove old records in the same request
145             data["deletions"] = [
146                 {
147                     "kind": "dns#resourceRecordSet",
148                     "type": "TXT",
149                     "name": record_name + ".",
150                     "rrdatas": record_contents,
151                     "ttl": record_ttl,
152                 },
153             ]
154 
155         changes = self.dns.changes()
156 
157         try:
158             request = changes.create(project=self.project_id, managedZone=zone_id, body=data)
159             response = request.execute()
160 
161             status = response['status']
162             change = response['id']
163             while status == 'pending':
164                 request = changes.get(project=self.project_id, managedZone=zone_id, changeId=change)
165                 response = request.execute()
166                 status = response['status']
167         except googleapiclient_errors.Error as e:
168             logger.error('Encountered error adding TXT record: %s', e)
169             raise errors.PluginError('Error communicating with the Google Cloud DNS API: {0}'
170                                      .format(e))
171 
172     def del_txt_record(self, domain, record_name, record_content, record_ttl):
173         """
174         Delete a TXT record using the supplied information.
175 
176         :param str domain: The domain to use to look up the managed zone.
177         :param str record_name: The record name (typically beginning with '_acme-challenge.').
178         :param str record_content: The record content (typically the challenge validation).
179         :param int record_ttl: The record TTL (number of seconds that the record may be cached).
180         :raises certbot.errors.PluginError: if an error occurs communicating with the Google API
181         """
182 
183         try:
184             zone_id = self._find_managed_zone_id(domain)
185         except errors.PluginError as e:
186             logger.warning('Error finding zone. Skipping cleanup.')
187             return
188 
189         record_contents = self.get_existing_txt_rrset(zone_id, record_name)
190         if record_contents is None:
191             record_contents = ["\"" + record_content + "\""]
192 
193         data = {
194             "kind": "dns#change",
195             "deletions": [
196                 {
197                     "kind": "dns#resourceRecordSet",
198                     "type": "TXT",
199                     "name": record_name + ".",
200                     "rrdatas": record_contents,
201                     "ttl": record_ttl,
202                 },
203             ],
204         }
205 
206         # Remove the record being deleted from the list
207         readd_contents = [r for r in record_contents if r != "\"" + record_content + "\""]
208         if readd_contents:
209             # We need to remove old records in the same request
210             data["additions"] = [
211                 {
212                     "kind": "dns#resourceRecordSet",
213                     "type": "TXT",
214                     "name": record_name + ".",
215                     "rrdatas": readd_contents,
216                     "ttl": record_ttl,
217                 },
218             ]
219 
220         changes = self.dns.changes()
221 
222         try:
223             request = changes.create(project=self.project_id, managedZone=zone_id, body=data)
224             request.execute()
225         except googleapiclient_errors.Error as e:
226             logger.warning('Encountered error deleting TXT record: %s', e)
227 
228     def get_existing_txt_rrset(self, zone_id, record_name):
229         """
230         Get existing TXT records from the RRset for the record name.
231 
232         If an error occurs while requesting the record set, it is suppressed
233         and None is returned.
234 
235         :param str zone_id: The ID of the managed zone.
236         :param str record_name: The record name (typically beginning with '_acme-challenge.').
237 
238         :returns: List of TXT record values or None
239         :rtype: `list` of `string` or `None`
240 
241         """
242         rrs_request = self.dns.resourceRecordSets()
243         request = rrs_request.list(managedZone=zone_id, project=self.project_id)
244         # Add dot as the API returns absolute domains
245         record_name += "."
246         try:
247             response = request.execute()
248         except googleapiclient_errors.Error:
249             logger.info("Unable to list existing records. If you're "
250                         "requesting a wildcard certificate, this might not work.")
251             logger.debug("Error was:", exc_info=True)
252         else:
253             if response:
254                 for rr in response["rrsets"]:
255                     if rr["name"] == record_name and rr["type"] == "TXT":
256                         return rr["rrdatas"]
257         return None
258 
259     def _find_managed_zone_id(self, domain):
260         """
261         Find the managed zone for a given domain.
262 
263         :param str domain: The domain for which to find the managed zone.
264         :returns: The ID of the managed zone, if found.
265         :rtype: str
266         :raises certbot.errors.PluginError: if the managed zone cannot be found.
267         """
268 
269         zone_dns_name_guesses = dns_common.base_domain_name_guesses(domain)
270 
271         mz = self.dns.managedZones()
272         for zone_name in zone_dns_name_guesses:
273             try:
274                 request = mz.list(project=self.project_id, dnsName=zone_name + '.')
275                 response = request.execute()
276                 zones = response['managedZones']
277             except googleapiclient_errors.Error as e:
278                 raise errors.PluginError('Encountered error finding managed zone: {0}'
279                                          .format(e))
280 
281             for zone in zones:
282                 zone_id = zone['id']
283                 if 'privateVisibilityConfig' not in zone:
284                     logger.debug('Found id of %s for %s using name %s', zone_id, domain, zone_name)
285                     return zone_id
286 
287         raise errors.PluginError('Unable to determine managed zone for {0} using zone names: {1}.'
288                                  .format(domain, zone_dns_name_guesses))
289 
290     @staticmethod
291     def get_project_id():
292         """
293         Query the google metadata service for the current project ID
294 
295         This only works on Google Cloud Platform
296 
297         :raises ServerNotFoundError: Not running on Google Compute or DNS not available
298         :raises ValueError: Server is found, but response code is not 200
299         :returns: project id
300         """
301         url = '{0}project/project-id'.format(METADATA_URL)
302 
303         # Request an access token from the metadata server.
304         http = httplib2.Http()
305         r, content = http.request(url, headers=METADATA_HEADERS)
306         if r.status != 200:
307             raise ValueError("Invalid status code: {0}".format(r))
308 
309         if isinstance(content, bytes):
310             return content.decode()
311         return content
312
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/certbot-dns-google/certbot_dns_google/_internal/dns_google.py b/certbot-dns-google/certbot_dns_google/_internal/dns_google.py
--- a/certbot-dns-google/certbot_dns_google/_internal/dns_google.py
+++ b/certbot-dns-google/certbot_dns_google/_internal/dns_google.py
@@ -240,9 +240,10 @@
 
         """
         rrs_request = self.dns.resourceRecordSets()
-        request = rrs_request.list(managedZone=zone_id, project=self.project_id)
         # Add dot as the API returns absolute domains
         record_name += "."
+        request = rrs_request.list(project=self.project_id, managedZone=zone_id, name=record_name,
+                                   type="TXT")
         try:
             response = request.execute()
         except googleapiclient_errors.Error:
@@ -250,10 +251,8 @@
                         "requesting a wildcard certificate, this might not work.")
             logger.debug("Error was:", exc_info=True)
         else:
-            if response:
-                for rr in response["rrsets"]:
-                    if rr["name"] == record_name and rr["type"] == "TXT":
-                        return rr["rrdatas"]
+            if response and response["rrsets"]:
+                return response["rrsets"][0]["rrdatas"]
         return None
 
     def _find_managed_zone_id(self, domain):

{"golden_diff": "diff --git a/certbot-dns-google/certbot_dns_google/_internal/dns_google.py b/certbot-dns-google/certbot_dns_google/_internal/dns_google.py\n--- a/certbot-dns-google/certbot_dns_google/_internal/dns_google.py\n+++ b/certbot-dns-google/certbot_dns_google/_internal/dns_google.py\n@@ -240,9 +240,10 @@\n \n         \"\"\"\n         rrs_request = self.dns.resourceRecordSets()\n-        request = rrs_request.list(managedZone=zone_id, project=self.project_id)\n         # Add dot as the API returns absolute domains\n         record_name += \".\"\n+        request = rrs_request.list(project=self.project_id, managedZone=zone_id, name=record_name,\n+                                   type=\"TXT\")\n         try:\n             response = request.execute()\n         except googleapiclient_errors.Error:\n@@ -250,10 +251,8 @@\n                         \"requesting a wildcard certificate, this might not work.\")\n             logger.debug(\"Error was:\", exc_info=True)\n         else:\n-            if response:\n-                for rr in response[\"rrsets\"]:\n-                    if rr[\"name\"] == record_name and rr[\"type\"] == \"TXT\":\n-                        return rr[\"rrdatas\"]\n+            if response and response[\"rrsets\"]:\n+                return response[\"rrsets\"][0][\"rrdatas\"]\n         return None\n \n     def _find_managed_zone_id(self, domain):\n", "issue": "Certbot-dns-google fails with HTTP 412 - Precondition not met\n## My operating system is (include version):\r\nCentOS 7\r\n\r\n## I installed Certbot with (certbot-auto, OS package manager, pip, etc):\r\nyum install -y certbot\r\npip install certbot-dns-google\r\n\r\n## I ran this command and it produced this output:\r\n\r\ncertbot certonly --dns-google --dns-google-credentials <path> --server https://acme-v02.api.letsencrypt.org/directory -d ssl1234.example.net -d *.example.net -d example.net  ...\r\n\r\n## Certbot's behavior differed from what I expected because:\r\n\r\nEncountered error adding TXT record: <HttpError 412 when requesting https://www.googleapis.com/dns/v1/projects/projectname/managedZones/id/changes?alt=json returned \"Precondition not met for 'entity.change.deletions[0]'\">\r\n\r\nThis problem only appeared if multiple domains are requested (around 15 SAN Names), single Domains work fine. I tested both wildcard domains and single domains - both fail sometimes. \r\n\r\n## Here is a Certbot log showing the issue (if available):\r\n###### Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.\r\n\r\n2018-05-24 16:05:21,163:WARNING:certbot_dns_google.dns_google:Encountered error deleting TXT record: <HttpError 412 when requesting https://www.googleapis.com/dns/v1/projects/projectname/managedZones/id/changes?alt=json returned \"Precondition not met for 'entity.change.deletions[0]'\">\r\n\n", "before_files": [{"content": "\"\"\"DNS Authenticator for Google Cloud DNS.\"\"\"\nimport json\nimport logging\n\nfrom googleapiclient import discovery\nfrom googleapiclient import errors as googleapiclient_errors\nimport httplib2\nfrom oauth2client.service_account import ServiceAccountCredentials\nimport zope.interface\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.plugins import dns_common\n\nlogger = logging.getLogger(__name__)\n\nACCT_URL = 'https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount'\nPERMISSIONS_URL = 'https://cloud.google.com/dns/access-control#permissions_and_roles'\nMETADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/'\nMETADATA_HEADERS = {'Metadata-Flavor': 'Google'}\n\n\n@zope.interface.implementer(interfaces.IAuthenticator)\n@zope.interface.provider(interfaces.IPluginFactory)\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator for Google Cloud DNS\n\n    This Authenticator uses the Google Cloud DNS API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = ('Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS '\n                   'for DNS).')\n    ttl = 60\n\n    def __init__(self, *args, **kwargs):\n        super(Authenticator, self).__init__(*args, **kwargs)\n        self.credentials = None\n\n    @classmethod\n    def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ\n        super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60)\n        add('credentials',\n            help=('Path to Google Cloud DNS service account JSON file. (See {0} for' +\n                  'information about creating a service account and {1} for information about the' +\n                  'required permissions.)').format(ACCT_URL, PERMISSIONS_URL),\n            default=None)\n\n    def more_info(self): # pylint: disable=missing-function-docstring\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the Google Cloud DNS API.'\n\n    def _setup_credentials(self):\n        if self.conf('credentials') is None:\n            try:\n                # use project_id query to check for availability of google metadata server\n                # we won't use the result but know we're not on GCP when an exception is thrown\n                _GoogleClient.get_project_id()\n            except (ValueError, httplib2.ServerNotFoundError):\n                raise errors.PluginError('Unable to get Google Cloud Metadata and no credentials'\n                                         ' specified. Automatic credential lookup is only '\n                                         'available on Google Cloud Platform. Please configure'\n                                         ' credentials using --dns-google-credentials <file>')\n        else:\n            self._configure_file('credentials',\n                                 'path to Google Cloud DNS service account JSON file')\n\n            dns_common.validate_file_permissions(self.conf('credentials'))\n\n    def _perform(self, domain, validation_name, validation):\n        self._get_google_client().add_txt_record(domain, validation_name, validation, self.ttl)\n\n    def _cleanup(self, domain, validation_name, validation):\n        self._get_google_client().del_txt_record(domain, validation_name, validation, self.ttl)\n\n    def _get_google_client(self):\n        return _GoogleClient(self.conf('credentials'))\n\n\nclass _GoogleClient(object):\n    \"\"\"\n    Encapsulates all communication with the Google Cloud DNS API.\n    \"\"\"\n\n    def __init__(self, account_json=None, dns_api=None):\n\n        scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']\n        if account_json is not None:\n            try:\n                credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)\n                with open(account_json) as account:\n                    self.project_id = json.load(account)['project_id']\n            except Exception as e:\n                raise errors.PluginError(\n                    \"Error parsing credentials file '{}': {}\".format(account_json, e))\n        else:\n            credentials = None\n            self.project_id = self.get_project_id()\n\n        if not dns_api:\n            self.dns = discovery.build('dns', 'v1',\n                                       credentials=credentials,\n                                       cache_discovery=False)\n        else:\n            self.dns = dns_api\n\n    def add_txt_record(self, domain, record_name, record_content, record_ttl):\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the Google API\n        \"\"\"\n\n        zone_id = self._find_managed_zone_id(domain)\n\n        record_contents = self.get_existing_txt_rrset(zone_id, record_name)\n        if record_contents is None:\n            record_contents = []\n        add_records = record_contents[:]\n\n        if \"\\\"\"+record_content+\"\\\"\" in record_contents:\n            # The process was interrupted previously and validation token exists\n            return\n\n        add_records.append(record_content)\n\n        data = {\n            \"kind\": \"dns#change\",\n            \"additions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": add_records,\n                    \"ttl\": record_ttl,\n                },\n            ],\n        }\n\n        if record_contents:\n            # We need to remove old records in the same request\n            data[\"deletions\"] = [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": record_contents,\n                    \"ttl\": record_ttl,\n                },\n            ]\n\n        changes = self.dns.changes()\n\n        try:\n            request = changes.create(project=self.project_id, managedZone=zone_id, body=data)\n            response = request.execute()\n\n            status = response['status']\n            change = response['id']\n            while status == 'pending':\n                request = changes.get(project=self.project_id, managedZone=zone_id, changeId=change)\n                response = request.execute()\n                status = response['status']\n        except googleapiclient_errors.Error as e:\n            logger.error('Encountered error adding TXT record: %s', e)\n            raise errors.PluginError('Error communicating with the Google Cloud DNS API: {0}'\n                                     .format(e))\n\n    def del_txt_record(self, domain, record_name, record_content, record_ttl):\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the Google API\n        \"\"\"\n\n        try:\n            zone_id = self._find_managed_zone_id(domain)\n        except errors.PluginError as e:\n            logger.warning('Error finding zone. Skipping cleanup.')\n            return\n\n        record_contents = self.get_existing_txt_rrset(zone_id, record_name)\n        if record_contents is None:\n            record_contents = [\"\\\"\" + record_content + \"\\\"\"]\n\n        data = {\n            \"kind\": \"dns#change\",\n            \"deletions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": record_contents,\n                    \"ttl\": record_ttl,\n                },\n            ],\n        }\n\n        # Remove the record being deleted from the list\n        readd_contents = [r for r in record_contents if r != \"\\\"\" + record_content + \"\\\"\"]\n        if readd_contents:\n            # We need to remove old records in the same request\n            data[\"additions\"] = [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": readd_contents,\n                    \"ttl\": record_ttl,\n                },\n            ]\n\n        changes = self.dns.changes()\n\n        try:\n            request = changes.create(project=self.project_id, managedZone=zone_id, body=data)\n            request.execute()\n        except googleapiclient_errors.Error as e:\n            logger.warning('Encountered error deleting TXT record: %s', e)\n\n    def get_existing_txt_rrset(self, zone_id, record_name):\n        \"\"\"\n        Get existing TXT records from the RRset for the record name.\n\n        If an error occurs while requesting the record set, it is suppressed\n        and None is returned.\n\n        :param str zone_id: The ID of the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n\n        :returns: List of TXT record values or None\n        :rtype: `list` of `string` or `None`\n\n        \"\"\"\n        rrs_request = self.dns.resourceRecordSets()\n        request = rrs_request.list(managedZone=zone_id, project=self.project_id)\n        # Add dot as the API returns absolute domains\n        record_name += \".\"\n        try:\n            response = request.execute()\n        except googleapiclient_errors.Error:\n            logger.info(\"Unable to list existing records. If you're \"\n                        \"requesting a wildcard certificate, this might not work.\")\n            logger.debug(\"Error was:\", exc_info=True)\n        else:\n            if response:\n                for rr in response[\"rrsets\"]:\n                    if rr[\"name\"] == record_name and rr[\"type\"] == \"TXT\":\n                        return rr[\"rrdatas\"]\n        return None\n\n    def _find_managed_zone_id(self, domain):\n        \"\"\"\n        Find the managed zone for a given domain.\n\n        :param str domain: The domain for which to find the managed zone.\n        :returns: The ID of the managed zone, if found.\n        :rtype: str\n        :raises certbot.errors.PluginError: if the managed zone cannot be found.\n        \"\"\"\n\n        zone_dns_name_guesses = dns_common.base_domain_name_guesses(domain)\n\n        mz = self.dns.managedZones()\n        for zone_name in zone_dns_name_guesses:\n            try:\n                request = mz.list(project=self.project_id, dnsName=zone_name + '.')\n                response = request.execute()\n                zones = response['managedZones']\n            except googleapiclient_errors.Error as e:\n                raise errors.PluginError('Encountered error finding managed zone: {0}'\n                                         .format(e))\n\n            for zone in zones:\n                zone_id = zone['id']\n                if 'privateVisibilityConfig' not in zone:\n                    logger.debug('Found id of %s for %s using name %s', zone_id, domain, zone_name)\n                    return zone_id\n\n        raise errors.PluginError('Unable to determine managed zone for {0} using zone names: {1}.'\n                                 .format(domain, zone_dns_name_guesses))\n\n    @staticmethod\n    def get_project_id():\n        \"\"\"\n        Query the google metadata service for the current project ID\n\n        This only works on Google Cloud Platform\n\n        :raises ServerNotFoundError: Not running on Google Compute or DNS not available\n        :raises ValueError: Server is found, but response code is not 200\n        :returns: project id\n        \"\"\"\n        url = '{0}project/project-id'.format(METADATA_URL)\n\n        # Request an access token from the metadata server.\n        http = httplib2.Http()\n        r, content = http.request(url, headers=METADATA_HEADERS)\n        if r.status != 200:\n            raise ValueError(\"Invalid status code: {0}\".format(r))\n\n        if isinstance(content, bytes):\n            return content.decode()\n        return content\n", "path": "certbot-dns-google/certbot_dns_google/_internal/dns_google.py"}], "after_files": [{"content": "\"\"\"DNS Authenticator for Google Cloud DNS.\"\"\"\nimport json\nimport logging\n\nfrom googleapiclient import discovery\nfrom googleapiclient import errors as googleapiclient_errors\nimport httplib2\nfrom oauth2client.service_account import ServiceAccountCredentials\nimport zope.interface\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.plugins import dns_common\n\nlogger = logging.getLogger(__name__)\n\nACCT_URL = 'https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount'\nPERMISSIONS_URL = 'https://cloud.google.com/dns/access-control#permissions_and_roles'\nMETADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/'\nMETADATA_HEADERS = {'Metadata-Flavor': 'Google'}\n\n\n@zope.interface.implementer(interfaces.IAuthenticator)\n@zope.interface.provider(interfaces.IPluginFactory)\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator for Google Cloud DNS\n\n    This Authenticator uses the Google Cloud DNS API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = ('Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS '\n                   'for DNS).')\n    ttl = 60\n\n    def __init__(self, *args, **kwargs):\n        super(Authenticator, self).__init__(*args, **kwargs)\n        self.credentials = None\n\n    @classmethod\n    def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ\n        super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60)\n        add('credentials',\n            help=('Path to Google Cloud DNS service account JSON file. (See {0} for' +\n                  'information about creating a service account and {1} for information about the' +\n                  'required permissions.)').format(ACCT_URL, PERMISSIONS_URL),\n            default=None)\n\n    def more_info(self): # pylint: disable=missing-function-docstring\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the Google Cloud DNS API.'\n\n    def _setup_credentials(self):\n        if self.conf('credentials') is None:\n            try:\n                # use project_id query to check for availability of google metadata server\n                # we won't use the result but know we're not on GCP when an exception is thrown\n                _GoogleClient.get_project_id()\n            except (ValueError, httplib2.ServerNotFoundError):\n                raise errors.PluginError('Unable to get Google Cloud Metadata and no credentials'\n                                         ' specified. Automatic credential lookup is only '\n                                         'available on Google Cloud Platform. Please configure'\n                                         ' credentials using --dns-google-credentials <file>')\n        else:\n            self._configure_file('credentials',\n                                 'path to Google Cloud DNS service account JSON file')\n\n            dns_common.validate_file_permissions(self.conf('credentials'))\n\n    def _perform(self, domain, validation_name, validation):\n        self._get_google_client().add_txt_record(domain, validation_name, validation, self.ttl)\n\n    def _cleanup(self, domain, validation_name, validation):\n        self._get_google_client().del_txt_record(domain, validation_name, validation, self.ttl)\n\n    def _get_google_client(self):\n        return _GoogleClient(self.conf('credentials'))\n\n\nclass _GoogleClient(object):\n    \"\"\"\n    Encapsulates all communication with the Google Cloud DNS API.\n    \"\"\"\n\n    def __init__(self, account_json=None, dns_api=None):\n\n        scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']\n        if account_json is not None:\n            try:\n                credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)\n                with open(account_json) as account:\n                    self.project_id = json.load(account)['project_id']\n            except Exception as e:\n                raise errors.PluginError(\n                    \"Error parsing credentials file '{}': {}\".format(account_json, e))\n        else:\n            credentials = None\n            self.project_id = self.get_project_id()\n\n        if not dns_api:\n            self.dns = discovery.build('dns', 'v1',\n                                       credentials=credentials,\n                                       cache_discovery=False)\n        else:\n            self.dns = dns_api\n\n    def add_txt_record(self, domain, record_name, record_content, record_ttl):\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the Google API\n        \"\"\"\n\n        zone_id = self._find_managed_zone_id(domain)\n\n        record_contents = self.get_existing_txt_rrset(zone_id, record_name)\n        if record_contents is None:\n            record_contents = []\n        add_records = record_contents[:]\n\n        if \"\\\"\"+record_content+\"\\\"\" in record_contents:\n            # The process was interrupted previously and validation token exists\n            return\n\n        add_records.append(record_content)\n\n        data = {\n            \"kind\": \"dns#change\",\n            \"additions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": add_records,\n                    \"ttl\": record_ttl,\n                },\n            ],\n        }\n\n        if record_contents:\n            # We need to remove old records in the same request\n            data[\"deletions\"] = [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": record_contents,\n                    \"ttl\": record_ttl,\n                },\n            ]\n\n        changes = self.dns.changes()\n\n        try:\n            request = changes.create(project=self.project_id, managedZone=zone_id, body=data)\n            response = request.execute()\n\n            status = response['status']\n            change = response['id']\n            while status == 'pending':\n                request = changes.get(project=self.project_id, managedZone=zone_id, changeId=change)\n                response = request.execute()\n                status = response['status']\n        except googleapiclient_errors.Error as e:\n            logger.error('Encountered error adding TXT record: %s', e)\n            raise errors.PluginError('Error communicating with the Google Cloud DNS API: {0}'\n                                     .format(e))\n\n    def del_txt_record(self, domain, record_name, record_content, record_ttl):\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the Google API\n        \"\"\"\n\n        try:\n            zone_id = self._find_managed_zone_id(domain)\n        except errors.PluginError as e:\n            logger.warning('Error finding zone. Skipping cleanup.')\n            return\n\n        record_contents = self.get_existing_txt_rrset(zone_id, record_name)\n        if record_contents is None:\n            record_contents = [\"\\\"\" + record_content + \"\\\"\"]\n\n        data = {\n            \"kind\": \"dns#change\",\n            \"deletions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": record_contents,\n                    \"ttl\": record_ttl,\n                },\n            ],\n        }\n\n        # Remove the record being deleted from the list\n        readd_contents = [r for r in record_contents if r != \"\\\"\" + record_content + \"\\\"\"]\n        if readd_contents:\n            # We need to remove old records in the same request\n            data[\"additions\"] = [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": readd_contents,\n                    \"ttl\": record_ttl,\n                },\n            ]\n\n        changes = self.dns.changes()\n\n        try:\n            request = changes.create(project=self.project_id, managedZone=zone_id, body=data)\n            request.execute()\n        except googleapiclient_errors.Error as e:\n            logger.warning('Encountered error deleting TXT record: %s', e)\n\n    def get_existing_txt_rrset(self, zone_id, record_name):\n        \"\"\"\n        Get existing TXT records from the RRset for the record name.\n\n        If an error occurs while requesting the record set, it is suppressed\n        and None is returned.\n\n        :param str zone_id: The ID of the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n\n        :returns: List of TXT record values or None\n        :rtype: `list` of `string` or `None`\n\n        \"\"\"\n        rrs_request = self.dns.resourceRecordSets()\n        # Add dot as the API returns absolute domains\n        record_name += \".\"\n        request = rrs_request.list(project=self.project_id, managedZone=zone_id, name=record_name,\n                                   type=\"TXT\")\n        try:\n            response = request.execute()\n        except googleapiclient_errors.Error:\n            logger.info(\"Unable to list existing records. If you're \"\n                        \"requesting a wildcard certificate, this might not work.\")\n            logger.debug(\"Error was:\", exc_info=True)\n        else:\n            if response and response[\"rrsets\"]:\n                return response[\"rrsets\"][0][\"rrdatas\"]\n        return None\n\n    def _find_managed_zone_id(self, domain):\n        \"\"\"\n        Find the managed zone for a given domain.\n\n        :param str domain: The domain for which to find the managed zone.\n        :returns: The ID of the managed zone, if found.\n        :rtype: str\n        :raises certbot.errors.PluginError: if the managed zone cannot be found.\n        \"\"\"\n\n        zone_dns_name_guesses = dns_common.base_domain_name_guesses(domain)\n\n        mz = self.dns.managedZones()\n        for zone_name in zone_dns_name_guesses:\n            try:\n                request = mz.list(project=self.project_id, dnsName=zone_name + '.')\n                response = request.execute()\n                zones = response['managedZones']\n            except googleapiclient_errors.Error as e:\n                raise errors.PluginError('Encountered error finding managed zone: {0}'\n                                         .format(e))\n\n            for zone in zones:\n                zone_id = zone['id']\n                if 'privateVisibilityConfig' not in zone:\n                    logger.debug('Found id of %s for %s using name %s', zone_id, domain, zone_name)\n                    return zone_id\n\n        raise errors.PluginError('Unable to determine managed zone for {0} using zone names: {1}.'\n                                 .format(domain, zone_dns_name_guesses))\n\n    @staticmethod\n    def get_project_id():\n        \"\"\"\n        Query the google metadata service for the current project ID\n\n        This only works on Google Cloud Platform\n\n        :raises ServerNotFoundError: Not running on Google Compute or DNS not available\n        :raises ValueError: Server is found, but response code is not 200\n        :returns: project id\n        \"\"\"\n        url = '{0}project/project-id'.format(METADATA_URL)\n\n        # Request an access token from the metadata server.\n        http = httplib2.Http()\n        r, content = http.request(url, headers=METADATA_HEADERS)\n        if r.status != 200:\n            raise ValueError(\"Invalid status code: {0}\".format(r))\n\n        if isinstance(content, bytes):\n            return content.decode()\n        return content\n", "path": "certbot-dns-google/certbot_dns_google/_internal/dns_google.py"}]}