1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3# 4# Copyright: Ansible Project 5# 6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 7 8from __future__ import absolute_import, division, print_function 9__metaclass__ = type 10 11 12DOCUMENTATION = ''' 13--- 14module: dnsimple 15short_description: Interface with dnsimple.com (a DNS hosting service) 16description: 17 - "Manages domains and records via the DNSimple API, see the docs: U(http://developer.dnsimple.com/)." 18options: 19 account_email: 20 description: 21 - Account email. If omitted, the environment variables C(DNSIMPLE_EMAIL) and C(DNSIMPLE_API_TOKEN) will be looked for. 22 - "If those aren't found, a C(.dnsimple) file will be looked for, see: U(https://github.com/mikemaccana/dnsimple-python#getting-started)." 23 - "C(.dnsimple) config files are only supported in dnsimple-python<2.0.0" 24 type: str 25 account_api_token: 26 description: 27 - Account API token. See I(account_email) for more information. 28 type: str 29 domain: 30 description: 31 - Domain to work with. Can be the domain name (e.g. "mydomain.com") or the numeric ID of the domain in DNSimple. 32 - If omitted, a list of domains will be returned. 33 - If domain is present but the domain doesn't exist, it will be created. 34 type: str 35 record: 36 description: 37 - Record to add, if blank a record for the domain will be created, supports the wildcard (*). 38 type: str 39 record_ids: 40 description: 41 - List of records to ensure they either exist or do not exist. 42 type: list 43 elements: str 44 type: 45 description: 46 - The type of DNS record to create. 47 choices: [ 'A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL', 'CAA' ] 48 type: str 49 ttl: 50 description: 51 - The TTL to give the new record in seconds. 52 default: 3600 53 type: int 54 value: 55 description: 56 - Record value. 57 - Must be specified when trying to ensure a record exists. 58 type: str 59 priority: 60 description: 61 - Record priority. 62 type: int 63 state: 64 description: 65 - whether the record should exist or not. 66 choices: [ 'present', 'absent' ] 67 default: present 68 type: str 69 solo: 70 description: 71 - Whether the record should be the only one for that record type and record name. 72 - Only use with C(state) is set to C(present) on a record. 73 type: 'bool' 74 default: no 75 sandbox: 76 description: 77 - Use the DNSimple sandbox environment. 78 - Requires a dedicated account in the dnsimple sandbox environment. 79 - Check U(https://developer.dnsimple.com/sandbox/) for more information. 80 type: 'bool' 81 default: no 82 version_added: 3.5.0 83requirements: 84 - "dnsimple >= 1.0.0" 85author: "Alex Coomans (@drcapulet)" 86''' 87 88EXAMPLES = ''' 89- name: Authenticate using email and API token and fetch all domains 90 community.general.dnsimple: 91 account_email: test@example.com 92 account_api_token: dummyapitoken 93 delegate_to: localhost 94 95- name: Fetch my.com domain records 96 community.general.dnsimple: 97 domain: my.com 98 state: present 99 delegate_to: localhost 100 register: records 101 102- name: Delete a domain 103 community.general.dnsimple: 104 domain: my.com 105 state: absent 106 delegate_to: localhost 107 108- name: Create a test.my.com A record to point to 127.0.0.1 109 community.general.dnsimple: 110 domain: my.com 111 record: test 112 type: A 113 value: 127.0.0.1 114 delegate_to: localhost 115 register: record 116 117- name: Delete record using record_ids 118 community.general.dnsimple: 119 domain: my.com 120 record_ids: '{{ record["id"] }}' 121 state: absent 122 delegate_to: localhost 123 124- name: Create a my.com CNAME record to example.com 125 community.general.dnsimple: 126 domain: my.com 127 record: '' 128 type: CNAME 129 value: example.com 130 state: present 131 delegate_to: localhost 132 133- name: Change TTL value for a record 134 community.general.dnsimple: 135 domain: my.com 136 record: '' 137 type: CNAME 138 value: example.com 139 ttl: 600 140 state: present 141 delegate_to: localhost 142 143- name: Delete the record 144 community.general.dnsimple: 145 domain: my.com 146 record: '' 147 type: CNAME 148 value: example.com 149 state: absent 150 delegate_to: localhost 151''' 152 153RETURN = r"""# """ 154 155import traceback 156from distutils.version import LooseVersion 157import re 158 159 160class DNSimpleV1(): 161 """class which uses dnsimple-python < 2""" 162 163 def __init__(self, account_email, account_api_token, sandbox, module): 164 """init""" 165 self.module = module 166 self.account_email = account_email 167 self.account_api_token = account_api_token 168 self.sandbox = sandbox 169 self.dnsimple_client() 170 171 def dnsimple_client(self): 172 """creates a dnsimple client object""" 173 if self.account_email and self.account_api_token: 174 self.client = DNSimple(sandbox=self.sandbox, email=self.account_email, api_token=self.account_api_token) 175 else: 176 self.client = DNSimple(sandbox=self.sandbox) 177 178 def get_all_domains(self): 179 """returns a list of all domains""" 180 domain_list = self.client.domains() 181 return [d['domain'] for d in domain_list] 182 183 def get_domain(self, domain): 184 """returns a single domain by name or id""" 185 try: 186 dr = self.client.domain(domain)['domain'] 187 except DNSimpleException as e: 188 exception_string = str(e.args[0]['message']) 189 if re.match(r"^Domain .+ not found$", exception_string): 190 dr = None 191 else: 192 raise 193 return dr 194 195 def create_domain(self, domain): 196 """create a single domain""" 197 return self.client.add_domain(domain)['domain'] 198 199 def delete_domain(self, domain): 200 """delete a single domain""" 201 self.client.delete(domain) 202 203 def get_records(self, domain, dnsimple_filter=None): 204 """return dns ressource records which match a specified filter""" 205 return [r['record'] for r in self.client.records(str(domain), params=dnsimple_filter)] 206 207 def delete_record(self, domain, rid): 208 """delete a single dns ressource record""" 209 self.client.delete_record(str(domain), rid) 210 211 def update_record(self, domain, rid, ttl=None, priority=None): 212 """update a single dns ressource record""" 213 data = {} 214 if ttl: 215 data['ttl'] = ttl 216 if priority: 217 data['priority'] = priority 218 return self.client.update_record(str(domain), str(rid), data)['record'] 219 220 def create_record(self, domain, name, record_type, content, ttl=None, priority=None): 221 """create a single dns ressource record""" 222 data = { 223 'name': name, 224 'type': record_type, 225 'content': content, 226 } 227 if ttl: 228 data['ttl'] = ttl 229 if priority: 230 data['priority'] = priority 231 return self.client.add_record(str(domain), data)['record'] 232 233 234class DNSimpleV2(): 235 """class which uses dnsimple-python >= 2""" 236 237 def __init__(self, account_email, account_api_token, sandbox, module): 238 """init""" 239 self.module = module 240 self.account_email = account_email 241 self.account_api_token = account_api_token 242 self.sandbox = sandbox 243 self.pagination_per_page = 30 244 self.dnsimple_client() 245 self.dnsimple_account() 246 247 def dnsimple_client(self): 248 """creates a dnsimple client object""" 249 if self.account_email and self.account_api_token: 250 client = Client(sandbox=self.sandbox, email=self.account_email, access_token=self.account_api_token) 251 else: 252 msg = "Option account_email or account_api_token not provided. " \ 253 "Dnsimple authentiction with a .dnsimple config file is not " \ 254 "supported with dnsimple-python>=2.0.0" 255 raise DNSimpleException(msg) 256 client.identity.whoami() 257 self.client = client 258 259 def dnsimple_account(self): 260 """select a dnsimple account. If a user token is used for authentication, 261 this user must only have access to a single account""" 262 account = self.client.identity.whoami().data.account 263 # user supplied a user token instead of account api token 264 if not account: 265 accounts = Accounts(self.client).list_accounts().data 266 if len(accounts) != 1: 267 msg = "The provided dnsimple token is a user token with multiple accounts." \ 268 "Use an account token or a user token with access to a single account." \ 269 "See https://support.dnsimple.com/articles/api-access-token/" 270 raise DNSimpleException(msg) 271 account = accounts[0] 272 self.account = account 273 274 def get_all_domains(self): 275 """returns a list of all domains""" 276 domain_list = self._get_paginated_result(self.client.domains.list_domains, account_id=self.account.id) 277 return [d.__dict__ for d in domain_list] 278 279 def get_domain(self, domain): 280 """returns a single domain by name or id""" 281 try: 282 dr = self.client.domains.get_domain(self.account.id, domain).data.__dict__ 283 except DNSimpleException as e: 284 exception_string = str(e.message) 285 if re.match(r"^Domain .+ not found$", exception_string): 286 dr = None 287 else: 288 raise 289 return dr 290 291 def create_domain(self, domain): 292 """create a single domain""" 293 return self.client.domains.create_domain(self.account.id, domain).data.__dict__ 294 295 def delete_domain(self, domain): 296 """delete a single domain""" 297 self.client.domains.delete_domain(self.account.id, domain) 298 299 def get_records(self, zone, dnsimple_filter=None): 300 """return dns ressource records which match a specified filter""" 301 records_list = self._get_paginated_result(self.client.zones.list_records, 302 account_id=self.account.id, 303 zone=zone, filter=dnsimple_filter) 304 return [d.__dict__ for d in records_list] 305 306 def delete_record(self, domain, rid): 307 """delete a single dns ressource record""" 308 self.client.zones.delete_record(self.account.id, domain, rid) 309 310 def update_record(self, domain, rid, ttl=None, priority=None): 311 """update a single dns ressource record""" 312 zr = ZoneRecordUpdateInput(ttl=ttl, priority=priority) 313 result = self.client.zones.update_record(self.account.id, str(domain), str(rid), zr).data.__dict__ 314 return result 315 316 def create_record(self, domain, name, record_type, content, ttl=None, priority=None): 317 """create a single dns ressource record""" 318 zr = ZoneRecordInput(name=name, type=record_type, content=content, ttl=ttl, priority=priority) 319 return self.client.zones.create_record(self.account.id, str(domain), zr).data.__dict__ 320 321 def _get_paginated_result(self, operation, **options): 322 """return all results of a paginated api response""" 323 records_pagination = operation(per_page=self.pagination_per_page, **options).pagination 324 result_list = [] 325 for page in range(1, records_pagination.total_pages + 1): 326 page_data = operation(per_page=self.pagination_per_page, page=page, **options).data 327 result_list.extend(page_data) 328 return result_list 329 330 331DNSIMPLE_IMP_ERR = [] 332HAS_DNSIMPLE = False 333try: 334 # try to import dnsimple >= 2.0.0 335 from dnsimple import Client, DNSimpleException 336 from dnsimple.service import Accounts 337 from dnsimple.version import version as dnsimple_version 338 from dnsimple.struct.zone_record import ZoneRecordUpdateInput, ZoneRecordInput 339 HAS_DNSIMPLE = True 340except ImportError: 341 DNSIMPLE_IMP_ERR.append(traceback.format_exc()) 342 343if not HAS_DNSIMPLE: 344 # try to import dnsimple < 2.0.0 345 try: 346 from dnsimple.dnsimple import __version__ as dnsimple_version 347 from dnsimple import DNSimple 348 from dnsimple.dnsimple import DNSimpleException 349 HAS_DNSIMPLE = True 350 except ImportError: 351 DNSIMPLE_IMP_ERR.append(traceback.format_exc()) 352 353from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback 354 355 356def main(): 357 module = AnsibleModule( 358 argument_spec=dict( 359 account_email=dict(type='str', fallback=(env_fallback, ['DNSIMPLE_EMAIL'])), 360 account_api_token=dict(type='str', 361 no_log=True, 362 fallback=(env_fallback, ['DNSIMPLE_API_TOKEN'])), 363 domain=dict(type='str'), 364 record=dict(type='str'), 365 record_ids=dict(type='list', elements='str'), 366 type=dict(type='str', choices=['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 367 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 368 'PTR', 'AAAA', 'SSHFP', 'HINFO', 369 'POOL', 'CAA']), 370 ttl=dict(type='int', default=3600), 371 value=dict(type='str'), 372 priority=dict(type='int'), 373 state=dict(type='str', choices=['present', 'absent'], default='present'), 374 solo=dict(type='bool', default=False), 375 sandbox=dict(type='bool', default=False), 376 ), 377 required_together=[ 378 ['record', 'value'] 379 ], 380 supports_check_mode=True, 381 ) 382 383 if not HAS_DNSIMPLE: 384 module.fail_json(msg=missing_required_lib('dnsimple'), exception=DNSIMPLE_IMP_ERR[0]) 385 386 account_email = module.params.get('account_email') 387 account_api_token = module.params.get('account_api_token') 388 domain = module.params.get('domain') 389 record = module.params.get('record') 390 record_ids = module.params.get('record_ids') 391 record_type = module.params.get('type') 392 ttl = module.params.get('ttl') 393 value = module.params.get('value') 394 priority = module.params.get('priority') 395 state = module.params.get('state') 396 is_solo = module.params.get('solo') 397 sandbox = module.params.get('sandbox') 398 399 DNSIMPLE_MAJOR_VERSION = LooseVersion(dnsimple_version).version[0] 400 401 try: 402 if DNSIMPLE_MAJOR_VERSION > 1: 403 ds = DNSimpleV2(account_email, account_api_token, sandbox, module) 404 else: 405 ds = DNSimpleV1(account_email, account_api_token, sandbox, module) 406 # Let's figure out what operation we want to do 407 # No domain, return a list 408 if not domain: 409 all_domains = ds.get_all_domains() 410 module.exit_json(changed=False, result=all_domains) 411 412 # Domain & No record 413 if record is None and not record_ids: 414 if domain.isdigit(): 415 typed_domain = int(domain) 416 else: 417 typed_domain = str(domain) 418 dr = ds.get_domain(typed_domain) 419 # domain does not exist 420 if state == 'present': 421 if dr: 422 module.exit_json(changed=False, result=dr) 423 else: 424 if module.check_mode: 425 module.exit_json(changed=True) 426 else: 427 response = ds.create_domain(domain) 428 module.exit_json(changed=True, result=response) 429 # state is absent 430 else: 431 if dr: 432 if not module.check_mode: 433 ds.delete_domain(domain) 434 module.exit_json(changed=True) 435 else: 436 module.exit_json(changed=False) 437 438 # need the not none check since record could be an empty string 439 if record is not None: 440 if not record_type: 441 module.fail_json(msg="Missing the record type") 442 if not value: 443 module.fail_json(msg="Missing the record value") 444 445 records_list = ds.get_records(domain, dnsimple_filter={'name': record}) 446 rr = next((r for r in records_list if r['name'] == record and r['type'] == record_type and r['content'] == value), None) 447 if state == 'present': 448 changed = False 449 if is_solo: 450 # delete any records that have the same name and record type 451 same_type = [r['id'] for r in records_list if r['name'] == record and r['type'] == record_type] 452 if rr: 453 same_type = [rid for rid in same_type if rid != rr['id']] 454 if same_type: 455 if not module.check_mode: 456 for rid in same_type: 457 ds.delete_record(domain, rid) 458 changed = True 459 if rr: 460 # check if we need to update 461 if rr['ttl'] != ttl or rr['priority'] != priority: 462 if module.check_mode: 463 module.exit_json(changed=True) 464 else: 465 response = ds.update_record(domain, rr['id'], ttl, priority) 466 module.exit_json(changed=True, result=response) 467 else: 468 module.exit_json(changed=changed, result=rr) 469 else: 470 # create it 471 if module.check_mode: 472 module.exit_json(changed=True) 473 else: 474 response = ds.create_record(domain, record, record_type, value, ttl, priority) 475 module.exit_json(changed=True, result=response) 476 # state is absent 477 else: 478 if rr: 479 if not module.check_mode: 480 ds.delete_record(domain, rr['id']) 481 module.exit_json(changed=True) 482 else: 483 module.exit_json(changed=False) 484 485 # Make sure these record_ids either all exist or none 486 if record_ids: 487 current_records = ds.get_records(domain, dnsimple_filter=None) 488 current_record_ids = [str(d['id']) for d in current_records] 489 wanted_record_ids = [str(r) for r in record_ids] 490 if state == 'present': 491 difference = list(set(wanted_record_ids) - set(current_record_ids)) 492 if difference: 493 module.fail_json(msg="Missing the following records: %s" % difference) 494 else: 495 module.exit_json(changed=False) 496 # state is absent 497 else: 498 difference = list(set(wanted_record_ids) & set(current_record_ids)) 499 if difference: 500 if not module.check_mode: 501 for rid in difference: 502 ds.delete_record(domain, rid) 503 module.exit_json(changed=True) 504 else: 505 module.exit_json(changed=False) 506 507 except DNSimpleException as e: 508 if DNSIMPLE_MAJOR_VERSION > 1: 509 module.fail_json(msg="DNSimple exception: %s" % e.message) 510 else: 511 module.fail_json(msg="DNSimple exception: %s" % str(e.args[0]['message'])) 512 module.fail_json(msg="Unknown what you wanted me to do") 513 514 515if __name__ == '__main__': 516 main() 517