├── certbot_dns_tencentcloud ├── __init__.py └── certbot_tencentcloud_plugins.py ├── Makefile ├── CHANGELOG.md ├── setup.py ├── LICENSE ├── .gitignore └── README.md /certbot_dns_tencentcloud/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: upload 2 | 3 | .PHONY: upload 4 | 5 | upload: 6 | rm -rf dist 7 | python setup.py sdist bdist_wheel 8 | twine upload --repository pypi dist/* 9 | 10 | 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.2 4 | 5 | compatibility fix thanks for osirisinferi 6 | 7 | ## 2.0.1 8 | 9 | - now cleanup record properly 10 | - improve README 11 | 12 | ## 2.0.0 13 | 14 | - Upgrade from old `cns` api to v3 `dnspod` api. From now on should only need to 15 | give **QcloudDNSPodFullAccess** strategy. 16 | 17 | ## 1.3.0 18 | 19 | - Now support setting `secret_id` and `secret_key` by environment (#3) 20 | 21 | ## 1.2.0 22 | 23 | - Add debug option 24 | 25 | ## 1.1.0 26 | 27 | - Fix incorrect base domain logic 28 | - Enhance error message 29 | - refactor 30 | 31 | ## 1.0.10 and before 32 | 33 | initial versions 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='certbot-dns-tencentcloud', 5 | version='2.0.2', 6 | author='Xiangyu Zhu', 7 | author_email='carsonzhu@tencent.com', 8 | description='Tencent Cloud DNS Authenticator plugin for Certbot', 9 | long_description=open('README.md').read(), 10 | long_description_content_type="text/markdown", 11 | url="https://github.com/frefreak/certbot-dns-tencentcloud", 12 | packages=find_packages(), 13 | include_package_data=True, 14 | install_requires=[ 15 | 'certbot>=1.18.0', 16 | ], 17 | classifiers=[ 18 | 'Environment :: Plugins', 19 | 'Intended Audience :: System Administrators', 20 | 'Operating System :: POSIX :: Linux', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 3.7', 23 | 'Programming Language :: Python :: 3.8', 24 | 'Programming Language :: Python :: 3.9', 25 | 'Programming Language :: Python :: 3.10', 26 | 'Programming Language :: Python :: 3.11', 27 | 'Topic :: Internet :: WWW/HTTP', 28 | 'Topic :: Security', 29 | 'Topic :: System :: Installation/Setup', 30 | 'Topic :: System :: Networking', 31 | 'Topic :: System :: Systems Administration', 32 | 'Topic :: Utilities', 33 | ], 34 | entry_points={ 35 | 'certbot.plugins': [ 36 | 'dns-tencentcloud = certbot_dns_tencentcloud.certbot_tencentcloud_plugins:Authenticator', 37 | ], 38 | }, 39 | ) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Xiangyu Zhu (c) 2020 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Xiangyu Zhu nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # certbot-dns-tencentcloud 2 | 3 | This package provides a Certbot authenticator plugin 4 | that can complete the DNS-01 challenge using the Tencent Cloud API. 5 | 6 | 7 | ## Installation 8 | 9 | Only Tested on python 3.8, should work on python 3.7 too and forward. 10 | 11 | - no plan to support python2 12 | - [dataclasses](https://docs.python.org/3/library/dataclasses.html) is used, so python 3.6 and down will not work. However you can try installing `dataclasses` from pypi. 13 | 14 | Use pip to install this package: 15 | ``` 16 | sudo pip3 install certbot-dns-tencentcloud 17 | ``` 18 | 19 | Verify the installation with Certbot: 20 | ``` 21 | sudo certbot plugins 22 | ``` 23 | You should see `dns-tencentcloud` in the output. 24 | 25 | 26 | ## Usage 27 | 28 | To use this plugin, set the authenticator to `dns-tencentcloud` via the `-a` or `--authenticator` flag. 29 | You may also set this using Certbot's configuration file (defaults to `/etc/letsencrypt/cli.ini`). 30 | 31 | You will also need to provide a credentials file with your Tencent Cloud API key id and secret, like the following: 32 | ``` 33 | dns_tencentcloud_secret_id = TENCENTCLOUD_SECRET_ID 34 | dns_tencentcloud_secret_key = TENCENTCLOUD_SECRET_KEY 35 | ``` 36 | The path to this file can be provided interactively or via the `--dns-tencentcloud-credentials` argument. 37 | 38 | You can also provide the credential using `TENCENTCLOUD_SECRET_ID` 39 | and `TENCENTCLOUD_SECRET_KEY` environment variables. 40 | 41 | **CAUTION:** 42 | Protect your API key as you would the password to your account. 43 | Anyone with access to this file can make API calls on your behalf. 44 | Be sure to **read the security tips below**. 45 | 46 | 47 | ### Arguments 48 | 49 | - `--dns-tencentcloud-credentials` path to Tencent Cloud credentials INI file (Required) 50 | - `--dns-tencentcloud-propagation-seconds` seconds to wait before verifying the DNS record (Default: 10) 51 | 52 | **NOTE:** Due to a [limitation in Certbot](https://github.com/certbot/certbot/issues/4351), 53 | these arguments *cannot* be set via Certbot's configuration file. 54 | 55 | 56 | ### Example 57 | 58 | When in root: 59 | 60 | ``` 61 | certbot certonly \ 62 | -a dns-tencentcloud \ 63 | --dns-tencentcloud-credentials ~/.secrets/certbot/tencentcloud.ini \ 64 | -d example.com 65 | ``` 66 | 67 | or if providing credentials using environment variable: 68 | 69 | ``` 70 | export TENCENTCLOUD_SECRET_ID= TENCENTCLOUD_SECRET_KEY= 71 | certbot certonly \ 72 | -a dns-tencentcloud \ 73 | -d example.com 74 | ``` 75 | 76 | 77 | ### Security Tips 78 | 79 | **Restrict access of your credentials file to the owner.** 80 | You can do this using `chmod 600`. 81 | Certbot will emit a warning if the credentials file 82 | can be accessed by other users on your system. 83 | 84 | **Use a separate key from your account's primary API key.** 85 | Make a separate user under your account, 86 | and limit its access to only allow DNS access 87 | and the IP address of the machine(s) that will be using it. 88 | 89 | ### FAQ 90 | 91 | 1. Which strategy should I choose to limit my API key access to only allow DNS resolution related operation? 92 | 93 | We now use the new DNSPOD api so you need to give `QcloudDNSPodFullAccess` strategy (need to add record so write permission is necessary). 94 | 95 | 2. renew certs for `*.abc.com` and `abc.com` at the same time sometimes show error about incorrect TXT records. 96 | 97 | It seems Let's Encrypt cache TXT records for at most 60 seconds, since DNSPod doesn't seem 98 | to allow setting TXT record's TTL below 60, in this case the best/safest way is to set 99 | `--dns-tencentcloud-propagation-seconds` longer than 60. 100 | 101 | 3. Debug mode? 102 | 103 | ``` 104 | --dns-tencentcloud-debug true 105 | ``` 106 | -------------------------------------------------------------------------------- /certbot_dns_tencentcloud/certbot_tencentcloud_plugins.py: -------------------------------------------------------------------------------- 1 | import json 2 | import hashlib 3 | import sys 4 | import random 5 | from datetime import datetime 6 | import os 7 | from typing import Dict, List 8 | from dataclasses import dataclass 9 | from hmac import HMAC 10 | from urllib.request import urlopen, Request 11 | 12 | from certbot import errors 13 | from certbot.plugins import dns_common 14 | 15 | 16 | class Authenticator(dns_common.DNSAuthenticator): 17 | """DNS Authenticator for TencentCloud 18 | 19 | This Authenticator uses the TencentCloud API to fulfill a dns-01 challenge. 20 | """ 21 | 22 | description = ( 23 | "Obtain certificates using a DNS TXT record (if you are " 24 | "using TencentCloud for DNS)." 25 | ) 26 | 27 | def __init__(self, *args, **kwargs): 28 | super().__init__(*args, **kwargs) 29 | self.secret_id = None 30 | self.secret_key = None 31 | self.cleanup_maps = {} 32 | 33 | @classmethod 34 | def add_parser_arguments(cls, add): # pylint: disable=arguments-differ 35 | super(Authenticator, cls).add_parser_arguments(add) 36 | add( 37 | "credentials", 38 | help="TencentCloud credentials INI file. If omitted, the environment variables TENCENTCLOUD_SECRET_ID and TENCENTCLOUD_SECRET_KEY will be tried", 39 | ) 40 | add( 41 | "debug", 42 | help="turn on debug mode (print some debug info)", 43 | type=bool, 44 | default=False, 45 | ) 46 | 47 | # pylint: disable=no-self-use 48 | def more_info(self): # pylint: disable=missing-function-docstring 49 | return ( 50 | "This plugin configures a DNS TXT record to respond to a dns-01 challenge using " 51 | + "the TencentCloud API." 52 | ) 53 | 54 | def _validate_credentials(self, credentials): 55 | self.chk_exist(credentials, "secret_id") 56 | self.chk_exist(credentials, "secret_key") 57 | 58 | def chk_exist(self, credentials, arg): 59 | v = credentials.conf(arg) 60 | if not v: 61 | raise errors.PluginError("{} is required".format(arg)) 62 | 63 | def chk_environ_exist(self, arg): 64 | if os.environ.get(arg) is None: 65 | print(os.environ) 66 | raise errors.PluginError("The environment {} is required".format(arg)) 67 | 68 | def chk_base_domain(self, base_domain, validation_name): 69 | if not validation_name.endswith("." + base_domain): 70 | raise errors.PluginError( 71 | "validation_name not ends with base domain name, please report to dev. " 72 | f"real_domain: {base_domain}, validation_name: {validation_name}" 73 | ) 74 | 75 | def determine_base_domain(self, domain): 76 | if self.conf("debug"): 77 | print("finding base domain") 78 | client = TencentCloudClient( 79 | self.secret_id, 80 | self.secret_key, 81 | self.conf("debug"), 82 | ) 83 | segments = domain.split(".") 84 | tried = [] 85 | i = len(segments) - 2 86 | while i >= 0: 87 | dt = ".".join(segments[i:]) 88 | tried.append(dt) 89 | i -= 1 90 | try: 91 | resp = client.describe_record_list(dt) 92 | # if error, we don't seem to own this domain 93 | except APIException as _: 94 | continue 95 | return dt, resp 96 | raise errors.PluginError( 97 | "failed to determine base domain, please report to dev. " f"Tried: {tried}" 98 | ) 99 | 100 | # pylint: enable=no-self-use 101 | 102 | def _setup_credentials(self): 103 | if self.conf("credentials"): 104 | credentials = self._configure_credentials( 105 | "credentials", 106 | "TencentCloud credentials INI file", 107 | None, 108 | self._validate_credentials, 109 | ) 110 | self.secret_id = credentials.conf("secret_id") 111 | self.secret_key = credentials.conf("secret_key") 112 | else: 113 | self.chk_environ_exist("TENCENTCLOUD_SECRET_ID") 114 | self.chk_environ_exist("TENCENTCLOUD_SECRET_KEY") 115 | self.secret_id = os.environ.get("TENCENTCLOUD_SECRET_ID") 116 | self.secret_key = os.environ.get("TENCENTCLOUD_SECRET_KEY") 117 | 118 | def _perform(self, domain, validation_name, validation): 119 | if self.conf("debug"): 120 | print("perform", domain, validation_name, validation) 121 | client = TencentCloudClient( 122 | self.secret_id, 123 | self.secret_key, 124 | self.conf("debug"), 125 | ) 126 | base_domain, _ = self.determine_base_domain(domain) 127 | self.chk_base_domain(base_domain, validation_name) 128 | 129 | sub_domain = validation_name[: -(len(base_domain) + 1)] 130 | r = client.create_record(base_domain, sub_domain, "TXT", validation) 131 | self.cleanup_maps[validation_name] = (base_domain, r["RecordId"]) 132 | 133 | def _cleanup(self, domain, validation_name, validation): 134 | if self.conf("debug"): 135 | print("cleanup", domain, validation_name, validation) 136 | client = TencentCloudClient( 137 | self.secret_id, 138 | self.secret_key, 139 | self.conf("debug"), 140 | ) 141 | if validation_name in self.cleanup_maps: 142 | base_domain, record_id = self.cleanup_maps[validation_name] 143 | client.delete_record(base_domain, record_id) 144 | else: 145 | print("record id not found during cleanup, cleanup probably failed") 146 | 147 | 148 | class APIException(Exception): 149 | pass 150 | 151 | 152 | class TencentCloudClient: 153 | """Simple specialized client for dnspod API.""" 154 | 155 | @dataclass 156 | class Cred: 157 | secret_id: str 158 | secret_key: str 159 | 160 | host = "dnspod.tencentcloudapi.com" 161 | url = "https://" + host 162 | algorithm = "TC3-HMAC-SHA256" 163 | version = "2021-03-23" 164 | 165 | def __init__(self, secret_id, secret_key, debug=False): 166 | self.cred = self.Cred(secret_id, secret_key) 167 | self.debug = debug 168 | 169 | def _mk_post_sign_v3(self, payload: Dict) -> Dict: 170 | now = datetime.now() 171 | now_timestamp = int(now.timestamp()) 172 | date = now.strftime("%Y-%m-%d") 173 | headers = { 174 | "Something-Random": random.getrandbits(64), 175 | "Content-Type": "application/json; charset=utf-8", 176 | "Host": self.host, 177 | "X-TC-Timestamp": now_timestamp, 178 | } 179 | canonical_headers = "\n".join( 180 | [k.lower() + ":" + str(headers[k]).lower() for k in sorted(headers)] 181 | ) 182 | signed_headers = ";".join([k.lower() for k in sorted(headers)]) 183 | hashed_request_payload = hashlib.sha256( 184 | json.dumps(payload).encode() 185 | ).hexdigest() 186 | canonical_request = [ 187 | "POST", 188 | "/", 189 | "", 190 | canonical_headers, 191 | "", 192 | signed_headers, 193 | hashed_request_payload, 194 | ] 195 | hashed_canonical_request = hashlib.sha256( 196 | "\n".join(canonical_request).encode() 197 | ).hexdigest() 198 | service, ending = ("dnspod", "tc3_request") 199 | credential_scope = f"{date}/{service}/{ending}" 200 | string_to_sign = "\n".join( 201 | [ 202 | self.algorithm, 203 | str(now_timestamp), 204 | credential_scope, 205 | hashed_canonical_request, 206 | ] 207 | ) 208 | secret_date = HMAC( 209 | ("TC3" + self.cred.secret_key).encode(), date.encode(), "sha256" 210 | ).digest() 211 | secret_service = HMAC(secret_date, service.encode(), "sha256").digest() 212 | secret_signing = HMAC(secret_service, ending.encode(), "sha256").digest() 213 | sig = HMAC(secret_signing, string_to_sign.encode(), "sha256").hexdigest() 214 | authorization = ( 215 | self.algorithm 216 | + " " 217 | + "Credential=" 218 | + self.cred.secret_id 219 | + "/" 220 | + credential_scope 221 | + ", " 222 | + "SignedHeaders=" 223 | + signed_headers 224 | + ", " 225 | + "Signature=" 226 | + sig 227 | ) 228 | headers["Authorization"] = authorization 229 | return headers 230 | 231 | def mk_post_req(self, action: str, payload: Dict) -> Dict: 232 | headers = self._mk_post_sign_v3(payload) 233 | headers["X-TC-Action"] = action 234 | headers["X-TC-Version"] = self.version 235 | request = Request(self.url, json.dumps(payload).encode(), headers) 236 | rj = json.loads(urlopen(request).read().decode()) 237 | resp = rj["Response"] 238 | if "Error" in resp: 239 | raise APIException(resp["Error"]) 240 | return resp 241 | 242 | def describe_domain(self, domain: str) -> Dict: 243 | payload = { 244 | "Domain": domain, 245 | } 246 | return self.mk_post_req("DescribeDomain", payload) 247 | 248 | def describe_record_list(self, domain: str) -> List[Dict]: 249 | offset = 0 250 | payload = { 251 | "Domain": domain, 252 | # the maximum allowed limit 253 | "Limit": 3000, 254 | "Offset": offset, 255 | } 256 | records = [] 257 | resp = self.mk_post_req("DescribeRecordList", payload) 258 | records.extend(resp["RecordList"]) 259 | while resp["RecordCountInfo"]["TotalCount"] > len(records): 260 | payload['Offset'] = len(records) 261 | resp = self.mk_post_req("DescribeRecordList", payload) 262 | records.extend(resp["RecordList"]) 263 | 264 | return records 265 | 266 | def create_record( 267 | self, domain: str, sub_domain: str, record_type: str, value: str 268 | ) -> Dict: 269 | payload = { 270 | "Domain": domain, 271 | "RecordType": record_type, 272 | "RecordLine": "默认", 273 | "SubDomain": sub_domain, 274 | "Value": value, 275 | } 276 | return self.mk_post_req("CreateRecord", payload) 277 | 278 | def modify_record( 279 | self, domain: str, rid: int, sub_domain: str, record_type: str, value: str 280 | ) -> Dict: 281 | payload = { 282 | "Domain": domain, 283 | "RecordType": record_type, 284 | "RecordLine": "默认", 285 | "SubDomain": sub_domain, 286 | "Value": value, 287 | "RecordId": rid, 288 | } 289 | return self.mk_post_req("ModifyRecord", payload) 290 | 291 | def delete_record(self, domain: str, rid: int): 292 | payload = { 293 | "Domain": domain, 294 | "RecordId": rid, 295 | } 296 | return self.mk_post_req("DeleteRecord", payload) 297 | 298 | 299 | if __name__ == "__main__": 300 | if len(sys.argv) != 2: 301 | print(f"Usage: {sys.argv[0]} ") 302 | sys.exit(1) 303 | domain = sys.argv[1] 304 | secret_id = os.getenv("TENCENTCLOUD_SECRET_ID") 305 | secret_key = os.getenv("TENCENTCLOUD_SECRET_KEY") 306 | if not secret_id or not secret_key: 307 | print("TENCENTCLOUD_SECRET_ID && TENCENTCLOUD_SECRET_KEY") 308 | sys.exit(1) 309 | cli = TencentCloudClient(secret_id, secret_key) 310 | r = cli.describe_domain(domain) 311 | sub = f"test-{random.getrandbits(32)}" 312 | 313 | print( 314 | "following operations might render your domain with un-cleaned up test record if something wrong happens in the middle." 315 | ) 316 | input("enter to continue...") 317 | 318 | print("creating record...") 319 | r = cli.create_record( 320 | domain, sub, "TXT", datetime.now().strftime("%Y%m%d %H:%M:%S") 321 | ) 322 | print( 323 | f"now please lookup TXT record of {sub}.{domain}, might need some secs to propagate" 324 | ) 325 | input("enter to continue...") 326 | 327 | print("modifying record...") 328 | r = cli.describe_record_list(domain) 329 | rid = None 330 | for rec in r: 331 | if rec["Name"] == sub: 332 | rid = rec["RecordId"] 333 | break 334 | if rid is None: 335 | print("weird, new record not found, exiting...") 336 | sys.exit(1) 337 | r = cli.modify_record( 338 | domain, rid, sub, "TXT", datetime.now().strftime("%Y%m%d %H:%M:%S") 339 | ) 340 | print( 341 | f"now please lookup TXT record of {sub}.{domain} again (probably need to wait ~60s)" 342 | ) 343 | input("enter to continue...") 344 | 345 | print("deleting record...") 346 | r = cli.delete_record(domain, rid) 347 | print("you can check now its deleted") 348 | --------------------------------------------------------------------------------