def create_txt_record(args): domain, token = args[0], args[2] zone_name = get_tld('http://' + domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(['_acme-challenge'] + domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) zone.add_record(node_name, record_type='TXT', txtdata=token, ttl=5) node = zone.get_node(node_name) zone.publish() logger.info(" + TXT record created: {0}".format(fqdn)) # give it 10 seconds to settle down and avoid nxdomain caching logger.info(" + Settling down for 10s...") time.sleep(10) retries=5 while(_has_dns_propagated(fqdn, token) == False and retries > 0): logger.info(" + DNS not propagated, waiting 30s...") retries-=1 time.sleep(30) if retries <= 0: logger.error("Error resolving TXT record for domain {0}".format(fqdn)) sys.exit(1)
def create_txt_record(domain, token, account_number): get_dynect_session() zone_name = get_zone_name(domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) try: # Delete all stale ACME TXT records delete_acme_txt_records(domain) except DynectGetError as e: if ("No such zone." in e.message or "Host is not in this zone" in e.message or "Host not found in this zone" in e.message): current_app.logger.debug( "Unable to delete ACME TXT records. They probably don't exist yet: {}" .format(e)) else: raise zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5) zone.publish() current_app.logger.debug("TXT record created: {0}".format(fqdn)) change_id = (fqdn, token) return change_id
def create_txt_record(domain, token, account_number): get_dynect_session() zone_name = get_zone_name(domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) try: zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5) zone.publish() current_app.logger.debug("TXT record created: {0}, token: {1}".format( fqdn, token)) except DynectCreateError as e: if "Cannot duplicate existing record data" in e.message: current_app.logger.debug( "Unable to add record. Domain: {}. Token: {}. " "Record already exists: {}".format(domain, token, e), exc_info=True) else: raise change_id = (fqdn, token) return change_id
def delete_txt_record(change_id, account_number, domain, token): get_dynect_session() if not domain: current_app.logger.debug("delete_txt_record: No domain passed") return zone_name = get_zone_name(domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) node = Node(zone_name, fqdn) try: all_txt_records = node.get_all_records_by_type('TXT') except DynectGetError: # No Text Records remain or host is not in the zone anymore because all records have been deleted. return for txt_record in all_txt_records: if txt_record.txtdata == ("{}".format(token)): current_app.logger.debug( "Deleting TXT record name: {0}".format(fqdn)) txt_record.delete() zone.publish()
def list_redirect(zone_name): """ Print information about redirects in a zone """ try: zone = Zone(zone_name) redirects = zone.get_all_httpredirect() except Exception as e: errordie("failed to get redirects for zone '{}': {}".format( zone_name, e)) # build list of redirects redirect_list = [] for redirect in redirects: redirect_list.append({redirect._fqdn: redirect._url}) # bail out if there weren't any redirects if len(redirect_list) == 0: return # build and output yaml document redirect_dict = [{ "webredirects": { "zone": zone_name, "redirects": redirect_list, }, }] print(yaml.safe_dump(redirect_dict, default_flow_style=False))
def delete_acme_txt_records(domain): get_dynect_session() if not domain: current_app.logger.debug("delete_acme_txt_records: No domain passed") return acme_challenge_string = "_acme-challenge" if not domain.startswith(acme_challenge_string): current_app.logger.debug( "delete_acme_txt_records: Domain {} doesn't start with string {}. " "Cowardly refusing to delete TXT records".format( domain, acme_challenge_string)) return zone_name = get_zone_name(domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) node = Node(zone_name, fqdn) all_txt_records = node.get_all_records_by_type('TXT') for txt_record in all_txt_records: current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn)) txt_record.delete() zone.publish()
def create_txt_record(domain, token, account_number): get_dynect_session() zone_name = get_zone_name(domain) zone_parts = len(zone_name.split(".")) node_name = ".".join(domain.split(".")[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) try: zone.add_record( node_name, record_type="TXT", txtdata='"{}"'.format(token), ttl=5 ) zone.publish() current_app.logger.debug( "TXT record created: {0}, token: {1}".format(fqdn, token) ) except (DynectCreateError, DynectUpdateError) as e: if "Cannot duplicate existing record data" in e.message: current_app.logger.debug( "Unable to add record. Domain: {}. Token: {}. " "Record already exists: {}".format(domain, token, e), exc_info=True, ) else: metrics.send("create_txt_record_error", "counter", 1) sentry.captureException() raise change_id = (fqdn, token) return change_id
def delete_acme_txt_records(domain): get_dynect_session() if not domain: current_app.logger.debug("delete_acme_txt_records: No domain passed") return acme_challenge_string = "_acme-challenge" if not domain.startswith(acme_challenge_string): current_app.logger.debug( "delete_acme_txt_records: Domain {} doesn't start with string {}. " "Cowardly refusing to delete TXT records".format(domain, acme_challenge_string)) return zone_name = get_zone_name(domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) node = Node(zone_name, fqdn) all_txt_records = node.get_all_records_by_type('TXT') for txt_record in all_txt_records: current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn)) try: txt_record.delete() except DynectDeleteError: sentry.captureException( extra={ "fqdn": str(fqdn), "zone_name": str(zone_name), "node_name": str(node_name), "txt_record": str(txt_record.txtdata)} ) metrics.send('delete_txt_record_deleteerror', 'counter', 1, metric_tags={'fqdn': fqdn, 'txt_record': txt_record.txtdata}) zone.publish()
def delete_txt_record(change_id, account_number, domain, token): get_dynect_session() if not domain: current_app.logger.debug("delete_txt_record: No domain passed") return zone_name = get_zone_name(domain) zone_parts = len(zone_name.split(".")) node_name = ".".join(domain.split(".")[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) node = Node(zone_name, fqdn) try: all_txt_records = node.get_all_records_by_type("TXT") except DynectGetError: metrics.send("delete_txt_record_geterror", "counter", 1) # No Text Records remain or host is not in the zone anymore because all records have been deleted. return for txt_record in all_txt_records: if txt_record.txtdata == ("{}".format(token)): current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn)) try: txt_record.delete() except DynectDeleteError: sentry.captureException( extra={ "fqdn": str(fqdn), "zone_name": str(zone_name), "node_name": str(node_name), "txt_record": str(txt_record.txtdata), } ) metrics.send( "delete_txt_record_deleteerror", "counter", 1, metric_tags={"fqdn": fqdn, "txt_record": txt_record.txtdata}, ) try: zone.publish() except DynectUpdateError: sentry.captureException( extra={ "fqdn": str(fqdn), "zone_name": str(zone_name), "node_name": str(node_name), "txt_record": str(txt_record.txtdata), } ) metrics.send( "delete_txt_record_publish_error", "counter", 1, metric_tags={"fqdn": str(fqdn), "txt_record": str(txt_record.txtdata)}, )
def list_zone(zone_name): """ Print names of a zone or all zones """ try: if zone_name == None: zones = get_all_zones() else: zones = [Zone(zone_name)] except Exception as e: errordie("failed to get zone(s): {}".format(e)) for zone in zones: print(zone.name)
def main(): args = docopt.docopt(__doc__) try: with DynectSession(os.environ["DYN_CUSTOMER"], os.environ["DYN_USERNAME"], os.environ["DYN_PASSWORD"]) as session: zone = Zone(args["<domainname>"]) create_route53_zone(args["<domainname>"], zone) except DynectGetError as exc: print(f"Zone could not be retrieved because of '{exc.message}'") except DynectAuthError as exc: print( f"Could not authenticate to dyn. Please check your environment variables" )
def operate_record(operation, zone_name, node_name, value, record_type_arg): """ Update address of a record """ record_type_map = { "a": "A", "cname": "CNAME", "mx": "MX", } record_type = record_type_map[record_type_arg] try: # get zone zone = Zone(zone_name) # update/delete if operation == "update" or operation == "delete": # if node_name is empty string then we're using the root node, use None if node_name == '': node = zone.get_node(None) else: node = zone.get_node(node_name) records = node.get_all_records_by_type(record_type) if not records: raise Exception("did not find {} records under {}".format( record_type, node.fqdn)) if operation == "update": records[0].address = value elif operation == "delete": records[0].delete() #create elif operation == "create": if record_type == 'A': kwargs = {'address': value} elif record_type == 'CNAME': kwargs = {'cname': value} elif record_type == 'MX': kwargs = {'exchange': value} zone.add_record(node_name, record_type, **kwargs) # publish changes to zone zone.publish() except Exception as e: errordie("Failed to make record change: {}".format(e))
def delete_txt_record(args): domain, token = args[0], args[2] if not domain: logger.info(" + http_request() error in letsencrypt.sh?") return zone_name = '.'.join(domain.split('.')[-2:]) node_name = "{0}.{1}".format('_acme-challenge', '.'.join(domain.split('.')[:-2])) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) node = Node(zone_name, fqdn) all_txt_records = node.get_all_records_by_type('TXT') for txt_record in all_txt_records: if txt_record.txtdata == (token): logger.info(" + Deleting TXT record name: {0}".format(fqdn)) txt_record.delete() zone.publish()
def list_record(zone_name, record_type_arg): """ Print information about records in a zone """ record_type_map = { "a": "a_records", "cname": "cname_records", "mx": "mx_records", } record_type = record_type_map[record_type_arg] try: zone = Zone(zone_name) records = zone.get_all_records() except Exception as e: errordie("failed to get records for zone '{}': {}".format( zone_name, e)) # bail out if there weren't any records of the requested type if record_type not in records: return # build list of records record_list = [] for record in records[record_type]: if record_type_arg == "a": value = record.address elif record_type_arg == "cname": value = record.cname elif record_type_arg == "mx": value = record.exchange record_list.append({record.fqdn: value}) # build and output yaml document recordset_dict = [{ "recordset": { "type": record_type_arg, "zone": zone_name, "records": record_list, }, }] print(yaml.safe_dump(recordset_dict, default_flow_style=False))
def delete_txt_record(change_id, account_number, domain, token): get_dynect_session() if not domain: current_app.logger.debug("delete_txt_record: No domain passed") return zone_name = get_zone_name(domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) node = Node(zone_name, fqdn) all_txt_records = node.get_all_records_by_type('TXT') for txt_record in all_txt_records: if txt_record.txtdata == ("{}".format(token)): current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn)) txt_record.delete() zone.publish()
def get_zone_records(self, zone_name): self.start_session() zone = Zone(zone_name) records = zone.get_all_records() self.close_session() return records
def main(): '''Ansible module for managing Dyn DNS records.''' module = AnsibleModule(argument_spec=dict( state=dict(required=True, choices=['present', 'absent', 'list']), customer_name=dict(default=os.environ.get('DYNECT_CUSTOMER_NAME', None), type='str'), user_name=dict(default=os.environ.get('DYNECT_USER_NAME', None), type='str', no_log=True), user_password=dict(default=os.environ.get('DYNECT_PASSWORD', None), type='str', no_log=True), zone=dict(required=True), record_fqdn=dict(required=False), record_type=dict(required=False, choices=['A', 'AAAA', 'CNAME', 'PTR', 'TXT']), record_value=dict(required=False), record_ttl=dict(required=False, default=0, type='int'), ), required_together=([ 'record_fqdn', 'record_value', 'record_ttl', 'record_type' ])) if IMPORT_ERROR: module.fail_json( msg="Unable to import dyn module: https://pypi.python.org/pypi/dyn", error=IMPORT_ERROR) # Start the Dyn session try: _ = DynectSession(module.params['customer_name'], module.params['user_name'], module.params['user_password']) except dyn.tm.errors.DynectAuthError as error: module.fail_json(msg='Unable to authenticate with Dyn', error=str(error)) # Retrieve zone object try: dyn_zone = Zone(module.params['zone']) except dyn.tm.errors.DynectGetError as error: if 'No such zone' in str(error): module.fail_json(msg="Not a valid zone for this account", zone=module.params['zone']) else: module.fail_json(msg="Unable to retrieve zone", error=str(error)) # To retrieve the node object we need to remove the zone name from the FQDN dyn_node_name = module.params['record_fqdn'].replace( '.' + module.params['zone'], '') # Retrieve the zone object from dyn dyn_zone = Zone(module.params['zone']) # Retrieve the node object from dyn dyn_node = dyn_zone.get_node(node=dyn_node_name) # All states will need a list of the exiting records for the zone. dyn_node_records = get_any_records(module, dyn_node) if module.params['state'] == 'list': module.exit_json(changed=False, records=get_record_values(dyn_node_records, )) if module.params['state'] == 'present': # First get a list of existing records for the node values = get_record_values(dyn_node_records) value_key = get_record_key(module.params['record_type']) param_value = module.params['record_value'] # Check to see if the record is already in place before doing anything. if (dyn_node_records and dyn_node_records[value_key][0].ttl == module.params['record_ttl'] and (param_value in values[value_key] or param_value + '.' in values[value_key])): module.exit_json(changed=False) # Working on the assumption that there is only one record per # node we will first delete the node if there are any records before # creating the correct record if dyn_node_records: dyn_node.delete() # Now lets create the correct node entry. dyn_zone.add_record(dyn_node_name, module.params['record_type'], module.params['record_value'], module.params['record_ttl']) # Now publish the zone since we've updated it. dyn_zone.publish() module.exit_json(changed=True, msg="Created node %s in zone %s" % (dyn_node_name, module.params['zone'])) if module.params['state'] == 'absent': # If there are any records present we'll want to delete the node. if dyn_node_records: dyn_node.delete() # Publish the zone since we've modified it. dyn_zone.publish() module.exit_json(changed=True, msg="Removed node %s from zone %s" % (dyn_node_name, module.params['zone'])) else: module.exit_json(changed=False)
def managed(name, domain, ipv4=[], ipv6=[]): ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''} if not name.endswith(domain): ret["comment"] = "Name is not a subdomain of domain" ret["result"] = False return ret node_name = name[:-len(domain) - 1] creds = __salt__["pillar.get"]("dynect:creds", None) if creds is None: ret["comment"] = "No Dynect Credentials Found" ret["result"] = True return ret # Filter down the ipv4 lists to only public addresses, we're assuming that # IPv6 addresses are always public. ipv4 = [i for i in ipv4 if not __salt__["network.is_private"](i)] # This is not a bug, there is global state at play here. DynectSession(creds["customer"], creds["user"], creds["password"]) zone = Zone(domain) node = zone.get_node(node_name) to_delete = [] to_add = [] try: # Look at all of the IPv4 Addresses for record in node.get_all_records_by_type("A"): if record.address in ipv4: ipv4.remove(record.address) else: to_delete.append(record) # Look at all of the IPv6 Addresses for record in node.get_all_records_by_type("AAAA"): if record.address in ipv6: ipv6.remove(record.address) else: to_delete.append(record) except dyn.tm.errors.DynectGetError: pass # Add any new IPv4 Addresses for address in ipv4: to_add.append((node_name, "A", address)) # Add any new IPv6 Addresses for address in ipv6: to_add.append((node_name, "AAAA", address)) if not to_delete and not to_add: ret['result'] = True ret["comment"] = "DNS for {} already correct.".format(name) return ret if __opts__['test'] == True: ret['comment'] = 'DNS for "{0}" will be changed.'.format(name) ret["changes"] = { "old": [str(s) for s in to_delete], "new": [str(s) for s in to_add], } ret["result"] = None return ret # Delete stuff for item in to_delete: item.delete() # Add stuff for item in to_add: zone.add_record(*item) # Publish! zone.publish() ret['comment'] = 'DNS for "{0}" was changed.'.format(name) ret["changes"] = { "old": [str(s) for s in to_delete], "new": [str(s) for s in to_add], } ret["result"] = True return ret
def main(): '''Ansible module for managing Dyn DNS records.''' module = AnsibleModule(argument_spec=dict( state=dict(default='present', choices=['present', 'absent', 'list']), customer_name=dict(default=os.environ.get('DYNECT_CUSTOMER_NAME', None), type='str'), user_name=dict(default=os.environ.get('DYNECT_USER_NAME', None), type='str', no_log=True), user_password=dict(default=os.environ.get('DYNECT_PASSWORD', None), type='str', no_log=True), zone=dict(required=True, type='str'), record_fqdn=dict(required=False, type='str'), record_type=dict(required=False, type='str', choices=['A', 'AAAA', 'CNAME', 'PTR', 'TXT']), record_value=dict(required=False, type='str'), record_ttl=dict(required=False, default=None, type='int'), use_zone_ttl=dict(required=False, default=False), ), required_together=([ 'record_fqdn', 'record_value', 'record_ttl', 'record_type' ]), mutually_exclusive=[('record_ttl', 'use_zone_ttl')]) if IMPORT_ERROR: module.fail_json( msg="Unable to import dyn module: https://pypi.python.org/pypi/dyn", error=IMPORT_ERROR) if module.params['record_ttl'] != None and int( module.params['record_ttl']) <= 0: module.fail_json(msg="Invalid Value for record TTL") # Start the Dyn session try: _ = DynectSession(module.params['customer_name'], module.params['user_name'], module.params['user_password']) except dyn.tm.errors.DynectAuthError as error: module.fail_json(msg='Unable to authenticate with Dyn', error=str(error)) # Retrieve zone object try: dyn_zone = Zone(module.params['zone']) except dyn.tm.errors.DynectGetError as error: if 'No such zone' in str(error): module.fail_json(msg="Not a valid zone for this account", zone=module.params['zone']) else: module.fail_json(msg="Unable to retrieve zone", error=str(error)) # To retrieve the node object we need to remove the zone name from the FQDN dyn_node_name = module.params['record_fqdn'].replace( '.' + module.params['zone'], '') # Retrieve the zone object from dyn dyn_zone = Zone(module.params['zone']) # Retrieve the node object from dyn dyn_node = dyn_zone.get_node(node=dyn_node_name) # All states will need a list of the exiting records for the zone. dyn_node_records = get_any_records(module, dyn_node) dyn_values = get_record_values(dyn_node_records) if module.params['state'] == 'list': module.exit_json(changed=False, dyn_records=dyn_values) elif module.params['state'] == 'absent': # If there are any records present we'll want to delete the node. if dyn_node_records: dyn_node.delete() # Publish the zone since we've modified it. dyn_zone.publish() module.exit_json(changed=True, msg="Removed node %s from zone %s" % (dyn_node_name, module.params['zone'])) module.exit_json(changed=False) elif module.params['state'] == 'present': # configure the TTL variable: # if use_zone_ttl, use the default TTL of the account. # if TTL == None, don't check it, set it as 0 (api default) # if TTL > 0, ensure this TTL is set if module.params['use_zone_ttl']: user_param_ttl = dyn_zone.ttl elif not module.params['record_ttl']: user_param_ttl = 0 else: user_param_ttl = module.params['record_ttl'] # First get a list of existing records for the node record_type_key = get_record_key(module.params['record_type']) user_record_value = module.params['record_value'] # Check to see if the record is already in place before doing anything. if dyn_node_records and compare_record_values( record_type_key, user_record_value, dyn_values): if user_param_ttl == 0 or \ compare_record_ttl(record_type_key, user_record_value, dyn_values, user_param_ttl): module.exit_json(changed=False, dyn_record=dyn_values) # Working on the assumption that there is only one record per # node we will first delete the node if there are any records before # creating the correct record if dyn_node_records: ###### # UPDATE ###### record = update_record_values(dyn_node_records, module.params['record_type'], module.params['record_value'], user_param_ttl) else: ###### # CREATE ###### # Now lets create the correct node entry. record = dyn_zone.add_record(dyn_node_name, module.params['record_type'], module.params['record_value'], user_param_ttl) # Now publish the zone since we've updated/created it. dyn_zone.publish() rmsg = "Created node {} in zone {}".format(dyn_node_name, module.params['zone']) module.exit_json(changed=True, msg=rmsg, dyn_record=get_record_values( {record_type_key: [record]})) module.fail_json(msg="Unknown state: [%s]" % module.params['state'])