mirror of
				https://github.com/github/octodns.git
				synced 2024-05-11 05:55:00 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			617 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			617 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#
 | 
						|
#
 | 
						|
#
 | 
						|
 | 
						|
from __future__ import (
 | 
						|
    absolute_import,
 | 
						|
    division,
 | 
						|
    print_function,
 | 
						|
    unicode_literals,
 | 
						|
)
 | 
						|
 | 
						|
from collections import defaultdict
 | 
						|
from requests import Session
 | 
						|
import http
 | 
						|
import logging
 | 
						|
import urllib.parse
 | 
						|
 | 
						|
from ..record import GeoCodes
 | 
						|
from ..record import Record
 | 
						|
from . import ProviderException
 | 
						|
from .base import BaseProvider
 | 
						|
 | 
						|
 | 
						|
class GCoreClientException(ProviderException):
 | 
						|
    def __init__(self, r):
 | 
						|
        super(GCoreClientException, self).__init__(r.text)
 | 
						|
 | 
						|
 | 
						|
class GCoreClientBadRequest(GCoreClientException):
 | 
						|
    def __init__(self, r):
 | 
						|
        super(GCoreClientBadRequest, self).__init__(r)
 | 
						|
 | 
						|
 | 
						|
class GCoreClientNotFound(GCoreClientException):
 | 
						|
    def __init__(self, r):
 | 
						|
        super(GCoreClientNotFound, self).__init__(r)
 | 
						|
 | 
						|
 | 
						|
class GCoreClient(object):
 | 
						|
 | 
						|
    ROOT_ZONES = "zones"
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        log,
 | 
						|
        api_url,
 | 
						|
        auth_url,
 | 
						|
        token=None,
 | 
						|
        token_type=None,
 | 
						|
        login=None,
 | 
						|
        password=None,
 | 
						|
    ):
 | 
						|
        self.log = log
 | 
						|
        self._session = Session()
 | 
						|
        self._api_url = api_url
 | 
						|
        if token is not None and token_type is not None:
 | 
						|
            self._session.headers.update(
 | 
						|
                {"Authorization": f"{token_type} {token}"}
 | 
						|
            )
 | 
						|
        elif login is not None and password is not None:
 | 
						|
            token = self._auth(auth_url, login, password)
 | 
						|
            self._session.headers.update(
 | 
						|
                {"Authorization": f"Bearer {token}"}
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            raise ValueError("either token or login & password must be set")
 | 
						|
 | 
						|
    def _auth(self, url, login, password):
 | 
						|
        # well, can't use _request, since API returns 400 if credentials
 | 
						|
        # invalid which will be logged, but we don't want do this
 | 
						|
        r = self._session.request(
 | 
						|
            "POST",
 | 
						|
            self._build_url(url, "auth", "jwt", "login"),
 | 
						|
            json={"username": login, "password": password},
 | 
						|
        )
 | 
						|
        r.raise_for_status()
 | 
						|
        return r.json()["access"]
 | 
						|
 | 
						|
    def _request(self, method, url, params=None, data=None):
 | 
						|
        r = self._session.request(
 | 
						|
            method, url, params=params, json=data, timeout=30.0
 | 
						|
        )
 | 
						|
        if r.status_code == http.HTTPStatus.BAD_REQUEST:
 | 
						|
            self.log.error(
 | 
						|
                "bad request %r has been sent to %r: %s", data, url, r.text
 | 
						|
            )
 | 
						|
            raise GCoreClientBadRequest(r)
 | 
						|
        elif r.status_code == http.HTTPStatus.NOT_FOUND:
 | 
						|
            self.log.error("resource %r not found: %s", url, r.text)
 | 
						|
            raise GCoreClientNotFound(r)
 | 
						|
        elif r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR:
 | 
						|
            self.log.error("server error no %r to %r: %s", data, url, r.text)
 | 
						|
            raise GCoreClientException(r)
 | 
						|
        r.raise_for_status()
 | 
						|
        return r
 | 
						|
 | 
						|
    def zone(self, zone_name):
 | 
						|
        return self._request(
 | 
						|
            "GET", self._build_url(self._api_url, self.ROOT_ZONES, zone_name)
 | 
						|
        ).json()
 | 
						|
 | 
						|
    def zone_create(self, zone_name):
 | 
						|
        return self._request(
 | 
						|
            "POST",
 | 
						|
            self._build_url(self._api_url, self.ROOT_ZONES),
 | 
						|
            data={"name": zone_name},
 | 
						|
        ).json()
 | 
						|
 | 
						|
    def zone_records(self, zone_name):
 | 
						|
        url = self._build_url(self._api_url, self.ROOT_ZONES, zone_name,
 | 
						|
                              "rrsets")
 | 
						|
        rrsets = self._request("GET", url, params={"all": "true"}).json()
 | 
						|
        records = rrsets["rrsets"]
 | 
						|
        return records
 | 
						|
 | 
						|
    def record_create(self, zone_name, rrset_name, type_, data):
 | 
						|
        self._request(
 | 
						|
            "POST", self._rrset_url(zone_name, rrset_name, type_), data=data
 | 
						|
        )
 | 
						|
 | 
						|
    def record_update(self, zone_name, rrset_name, type_, data):
 | 
						|
        self._request(
 | 
						|
            "PUT", self._rrset_url(zone_name, rrset_name, type_), data=data
 | 
						|
        )
 | 
						|
 | 
						|
    def record_delete(self, zone_name, rrset_name, type_):
 | 
						|
        self._request("DELETE", self._rrset_url(zone_name, rrset_name, type_))
 | 
						|
 | 
						|
    def _rrset_url(self, zone_name, rrset_name, type_):
 | 
						|
        return self._build_url(
 | 
						|
            self._api_url, self.ROOT_ZONES, zone_name, rrset_name, type_
 | 
						|
        )
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _build_url(base, *items):
 | 
						|
        for i in items:
 | 
						|
            base = base.strip("/") + "/"
 | 
						|
            base = urllib.parse.urljoin(base, i)
 | 
						|
        return base
 | 
						|
 | 
						|
 | 
						|
class GCoreProvider(BaseProvider):
 | 
						|
    """
 | 
						|
    GCore provider using API v2.
 | 
						|
 | 
						|
    gcore:
 | 
						|
        class: octodns.provider.gcore.GCoreProvider
 | 
						|
        # Your API key
 | 
						|
        token: XXXXXXXXXXXX
 | 
						|
        # token_type: APIKey
 | 
						|
        # or login + password
 | 
						|
        login: XXXXXXXXXXXX
 | 
						|
        password: XXXXXXXXXXXX
 | 
						|
        # auth_url: https://api.gcdn.co
 | 
						|
        # url: https://dnsapi.gcorelabs.com/v2
 | 
						|
        # records_per_response: 1
 | 
						|
    """
 | 
						|
 | 
						|
    SUPPORTS_GEO = False
 | 
						|
    SUPPORTS_DYNAMIC = True
 | 
						|
    SUPPORTS = set(("A", "AAAA", "NS", "MX", "TXT", "SRV", "CNAME", "PTR"))
 | 
						|
 | 
						|
    def __init__(self, id, *args, **kwargs):
 | 
						|
        token = kwargs.pop("token", None)
 | 
						|
        token_type = kwargs.pop("token_type", "APIKey")
 | 
						|
        login = kwargs.pop("login", None)
 | 
						|
        password = kwargs.pop("password", None)
 | 
						|
        api_url = kwargs.pop("url", "https://dnsapi.gcorelabs.com/v2")
 | 
						|
        auth_url = kwargs.pop("auth_url", "https://api.gcdn.co")
 | 
						|
        self.records_per_response = kwargs.pop("records_per_response", 1)
 | 
						|
        self.log = logging.getLogger(f"GCoreProvider[{id}]")
 | 
						|
        self.log.debug("__init__: id=%s", id)
 | 
						|
        super(GCoreProvider, self).__init__(id, *args, **kwargs)
 | 
						|
        self._client = GCoreClient(
 | 
						|
            self.log,
 | 
						|
            api_url,
 | 
						|
            auth_url,
 | 
						|
            token=token,
 | 
						|
            token_type=token_type,
 | 
						|
            login=login,
 | 
						|
            password=password,
 | 
						|
        )
 | 
						|
 | 
						|
    def _add_dot_if_need(self, value):
 | 
						|
        return f"{value}." if not value.endswith(".") else value
 | 
						|
 | 
						|
    def _build_pools(self, record, default_pool_name, value_transform_fn):
 | 
						|
        defaults = []
 | 
						|
        geo_sets, pool_idx = dict(), 0
 | 
						|
        pools = defaultdict(lambda: {"values": []})
 | 
						|
        for rr in record["resource_records"]:
 | 
						|
            meta = rr.get("meta", {}) or {}
 | 
						|
            value = {"value": value_transform_fn(rr["content"][0])}
 | 
						|
            countries = meta.get("countries", []) or []
 | 
						|
            continents = meta.get("continents", []) or []
 | 
						|
 | 
						|
            if meta.get("default", False):
 | 
						|
                pools[default_pool_name]["values"].append(value)
 | 
						|
                defaults.append(value["value"])
 | 
						|
                continue
 | 
						|
            # defaults is false or missing and no conties or continents
 | 
						|
            elif len(continents) == 0 and len(countries) == 0:
 | 
						|
                defaults.append(value["value"])
 | 
						|
                continue
 | 
						|
 | 
						|
            # RR with the same set of countries and continents are
 | 
						|
            # combined in single pool
 | 
						|
            geo_set = frozenset(
 | 
						|
                [GeoCodes.country_to_code(cc.upper()) for cc in countries]
 | 
						|
            ) | frozenset(cc.upper() for cc in continents)
 | 
						|
            if geo_set not in geo_sets:
 | 
						|
                geo_sets[geo_set] = f"pool-{pool_idx}"
 | 
						|
                pool_idx += 1
 | 
						|
 | 
						|
            pools[geo_sets[geo_set]]["values"].append(value)
 | 
						|
 | 
						|
        return pools, geo_sets, defaults
 | 
						|
 | 
						|
    def _build_rules(self, pools, geo_sets):
 | 
						|
        rules = []
 | 
						|
        for name, _ in pools.items():
 | 
						|
            rule = {"pool": name}
 | 
						|
            geo_set = next(
 | 
						|
                (
 | 
						|
                    geo_set
 | 
						|
                    for geo_set, pool_name in geo_sets.items()
 | 
						|
                    if pool_name == name
 | 
						|
                ),
 | 
						|
                {},
 | 
						|
            )
 | 
						|
            if len(geo_set) > 0:
 | 
						|
                rule["geos"] = list(geo_set)
 | 
						|
            rules.append(rule)
 | 
						|
 | 
						|
        return sorted(rules, key=lambda x: x["pool"])
 | 
						|
 | 
						|
    def _data_for_dynamic(self, record, value_transform_fn=lambda x: x):
 | 
						|
        default_pool = "other"
 | 
						|
        pools, geo_sets, defaults = self._build_pools(
 | 
						|
            record, default_pool, value_transform_fn
 | 
						|
        )
 | 
						|
        if len(pools) == 0:
 | 
						|
            raise RuntimeError(
 | 
						|
                f"filter is enabled, but no pools where built for {record}"
 | 
						|
            )
 | 
						|
 | 
						|
        # defaults can't be empty, so use first pool values
 | 
						|
        if len(defaults) == 0:
 | 
						|
            defaults = [
 | 
						|
                value_transform_fn(v["value"])
 | 
						|
                for v in next(iter(pools.values()))["values"]
 | 
						|
            ]
 | 
						|
 | 
						|
        # if at least one default RR was found then setup fallback for
 | 
						|
        # other pools to default
 | 
						|
        if default_pool in pools:
 | 
						|
            for pool_name, pool in pools.items():
 | 
						|
                if pool_name == default_pool:
 | 
						|
                    continue
 | 
						|
                pool["fallback"] = default_pool
 | 
						|
 | 
						|
        rules = self._build_rules(pools, geo_sets)
 | 
						|
        return pools, rules, defaults
 | 
						|
 | 
						|
    def _data_for_single(self, _type, record):
 | 
						|
        return {
 | 
						|
            "ttl": record["ttl"],
 | 
						|
            "type": _type,
 | 
						|
            "value": self._add_dot_if_need(
 | 
						|
                record["resource_records"][0]["content"][0]
 | 
						|
            ),
 | 
						|
        }
 | 
						|
 | 
						|
    _data_for_PTR = _data_for_single
 | 
						|
 | 
						|
    def _data_for_CNAME(self, _type, record):
 | 
						|
        if record.get("filters") is None:
 | 
						|
            return self._data_for_single(_type, record)
 | 
						|
 | 
						|
        pools, rules, defaults = self._data_for_dynamic(
 | 
						|
            record, self._add_dot_if_need
 | 
						|
        )
 | 
						|
        return {
 | 
						|
            "ttl": record["ttl"],
 | 
						|
            "type": _type,
 | 
						|
            "dynamic": {"pools": pools, "rules": rules},
 | 
						|
            "value": self._add_dot_if_need(defaults[0]),
 | 
						|
        }
 | 
						|
 | 
						|
    def _data_for_multiple(self, _type, record):
 | 
						|
        extra = dict()
 | 
						|
        if record.get("filters") is not None:
 | 
						|
            pools, rules, defaults = self._data_for_dynamic(record)
 | 
						|
            extra = {
 | 
						|
                "dynamic": {"pools": pools, "rules": rules},
 | 
						|
                "values": defaults,
 | 
						|
            }
 | 
						|
        else:
 | 
						|
            extra = {
 | 
						|
                "values": [
 | 
						|
                    rr_value
 | 
						|
                    for resource_record in record["resource_records"]
 | 
						|
                    for rr_value in resource_record["content"]
 | 
						|
                ]
 | 
						|
            }
 | 
						|
        return {
 | 
						|
            "ttl": record["ttl"],
 | 
						|
            "type": _type,
 | 
						|
            **extra,
 | 
						|
        }
 | 
						|
 | 
						|
    _data_for_A = _data_for_multiple
 | 
						|
    _data_for_AAAA = _data_for_multiple
 | 
						|
 | 
						|
    def _data_for_TXT(self, _type, record):
 | 
						|
        return {
 | 
						|
            "ttl": record["ttl"],
 | 
						|
            "type": _type,
 | 
						|
            "values": [
 | 
						|
                rr_value.replace(";", "\\;")
 | 
						|
                for resource_record in record["resource_records"]
 | 
						|
                for rr_value in resource_record["content"]
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def _data_for_MX(self, _type, record):
 | 
						|
        return {
 | 
						|
            "ttl": record["ttl"],
 | 
						|
            "type": _type,
 | 
						|
            "values": [
 | 
						|
                dict(
 | 
						|
                    preference=preference,
 | 
						|
                    exchange=self._add_dot_if_need(exchange),
 | 
						|
                )
 | 
						|
                for preference, exchange in map(
 | 
						|
                    lambda x: x["content"], record["resource_records"]
 | 
						|
                )
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def _data_for_NS(self, _type, record):
 | 
						|
        return {
 | 
						|
            "ttl": record["ttl"],
 | 
						|
            "type": _type,
 | 
						|
            "values": [
 | 
						|
                self._add_dot_if_need(rr_value)
 | 
						|
                for resource_record in record["resource_records"]
 | 
						|
                for rr_value in resource_record["content"]
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def _data_for_SRV(self, _type, record):
 | 
						|
        return {
 | 
						|
            "ttl": record["ttl"],
 | 
						|
            "type": _type,
 | 
						|
            "values": [
 | 
						|
                dict(
 | 
						|
                    priority=priority,
 | 
						|
                    weight=weight,
 | 
						|
                    port=port,
 | 
						|
                    target=self._add_dot_if_need(target),
 | 
						|
                )
 | 
						|
                for priority, weight, port, target in map(
 | 
						|
                    lambda x: x["content"], record["resource_records"]
 | 
						|
                )
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def zone_records(self, zone):
 | 
						|
        try:
 | 
						|
            return self._client.zone_records(zone.name[:-1]), True
 | 
						|
        except GCoreClientNotFound:
 | 
						|
            return [], False
 | 
						|
 | 
						|
    def populate(self, zone, target=False, lenient=False):
 | 
						|
        self.log.debug(
 | 
						|
            "populate: name=%s, target=%s, lenient=%s",
 | 
						|
            zone.name,
 | 
						|
            target,
 | 
						|
            lenient,
 | 
						|
        )
 | 
						|
 | 
						|
        values = defaultdict(defaultdict)
 | 
						|
        records, exists = self.zone_records(zone)
 | 
						|
        for record in records:
 | 
						|
            _type = record["type"].upper()
 | 
						|
            if _type not in self.SUPPORTS:
 | 
						|
                continue
 | 
						|
            if self._should_ignore(record):
 | 
						|
                continue
 | 
						|
            rr_name = zone.hostname_from_fqdn(record["name"])
 | 
						|
            values[rr_name][_type] = record
 | 
						|
 | 
						|
        before = len(zone.records)
 | 
						|
        for name, types in values.items():
 | 
						|
            for _type, record in types.items():
 | 
						|
                data_for = getattr(self, f"_data_for_{_type}")
 | 
						|
                record = Record.new(
 | 
						|
                    zone,
 | 
						|
                    name,
 | 
						|
                    data_for(_type, record),
 | 
						|
                    source=self,
 | 
						|
                    lenient=lenient,
 | 
						|
                )
 | 
						|
                zone.add_record(record, lenient=lenient)
 | 
						|
 | 
						|
        self.log.info(
 | 
						|
            "populate:   found %s records, exists=%s",
 | 
						|
            len(zone.records) - before,
 | 
						|
            exists,
 | 
						|
        )
 | 
						|
        return exists
 | 
						|
 | 
						|
    def _should_ignore(self, record):
 | 
						|
        name = record.get("name", "name-not-defined")
 | 
						|
        if record.get("filters") is None:
 | 
						|
            return False
 | 
						|
        want_filters = 3
 | 
						|
        filters = record.get("filters", [])
 | 
						|
        if len(filters) != want_filters:
 | 
						|
            self.log.info(
 | 
						|
                "ignore %s has filters and their count is not %d",
 | 
						|
                name,
 | 
						|
                want_filters,
 | 
						|
            )
 | 
						|
            return True
 | 
						|
        types = [v.get("type") for v in filters]
 | 
						|
        for i, want_type in enumerate(["geodns", "default", "first_n"]):
 | 
						|
            if types[i] != want_type:
 | 
						|
                self.log.info(
 | 
						|
                    "ignore %s, filters.%d.type is %s, want %s",
 | 
						|
                    name,
 | 
						|
                    i,
 | 
						|
                    types[i],
 | 
						|
                    want_type,
 | 
						|
                )
 | 
						|
                return True
 | 
						|
        limits = [filters[i].get("limit", 1) for i in [1, 2]]
 | 
						|
        if limits[0] != limits[1]:
 | 
						|
            self.log.info(
 | 
						|
                "ignore %s, filters.1.limit (%d) != filters.2.limit (%d)",
 | 
						|
                name,
 | 
						|
                limits[0],
 | 
						|
                limits[1],
 | 
						|
            )
 | 
						|
            return True
 | 
						|
        return False
 | 
						|
 | 
						|
    def _params_for_dymanic(self, record):
 | 
						|
        records = []
 | 
						|
        default_pool_found = False
 | 
						|
        default_values = set(
 | 
						|
            record.values if hasattr(record, "values") else [record.value]
 | 
						|
        )
 | 
						|
        for rule in record.dynamic.rules:
 | 
						|
            meta = dict()
 | 
						|
            # build meta tags if geos information present
 | 
						|
            if len(rule.data.get("geos", [])) > 0:
 | 
						|
                for geo_code in rule.data["geos"]:
 | 
						|
                    geo = GeoCodes.parse(geo_code)
 | 
						|
 | 
						|
                    country = geo["country_code"]
 | 
						|
                    continent = geo["continent_code"]
 | 
						|
                    if country is not None:
 | 
						|
                        meta.setdefault("countries", []).append(country)
 | 
						|
                    else:
 | 
						|
                        meta.setdefault("continents", []).append(continent)
 | 
						|
            else:
 | 
						|
                meta["default"] = True
 | 
						|
 | 
						|
            pool_values = set()
 | 
						|
            pool_name = rule.data["pool"]
 | 
						|
            for value in record.dynamic.pools[pool_name].data["values"]:
 | 
						|
                v = value["value"]
 | 
						|
                records.append({"content": [v], "meta": meta})
 | 
						|
                pool_values.add(v)
 | 
						|
 | 
						|
            default_pool_found |= default_values == pool_values
 | 
						|
 | 
						|
        # if default values doesn't match any pool values, then just add this
 | 
						|
        # values with no any meta
 | 
						|
        if not default_pool_found:
 | 
						|
            for value in default_values:
 | 
						|
                records.append({"content": [value]})
 | 
						|
 | 
						|
        return records
 | 
						|
 | 
						|
    def _params_for_single(self, record):
 | 
						|
        return {
 | 
						|
            "ttl": record.ttl,
 | 
						|
            "resource_records": [{"content": [record.value]}],
 | 
						|
        }
 | 
						|
 | 
						|
    _params_for_PTR = _params_for_single
 | 
						|
 | 
						|
    def _params_for_CNAME(self, record):
 | 
						|
        if not record.dynamic:
 | 
						|
            return self._params_for_single(record)
 | 
						|
 | 
						|
        return {
 | 
						|
            "ttl": record.ttl,
 | 
						|
            "resource_records": self._params_for_dymanic(record),
 | 
						|
            "filters": [
 | 
						|
                {"type": "geodns"},
 | 
						|
                {
 | 
						|
                    "type": "default",
 | 
						|
                    "limit": self.records_per_response,
 | 
						|
                    "strict": False,
 | 
						|
                },
 | 
						|
                {"type": "first_n", "limit": self.records_per_response},
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def _params_for_multiple(self, record):
 | 
						|
        extra = dict()
 | 
						|
        if record.dynamic:
 | 
						|
            extra["resource_records"] = self._params_for_dymanic(record)
 | 
						|
            extra["filters"] = [
 | 
						|
                {"type": "geodns"},
 | 
						|
                {
 | 
						|
                    "type": "default",
 | 
						|
                    "limit": self.records_per_response,
 | 
						|
                    "strict": False,
 | 
						|
                },
 | 
						|
                {"type": "first_n", "limit": self.records_per_response},
 | 
						|
            ]
 | 
						|
        else:
 | 
						|
            extra["resource_records"] = [
 | 
						|
                {"content": [value]} for value in record.values
 | 
						|
            ]
 | 
						|
        return {
 | 
						|
            "ttl": record.ttl,
 | 
						|
            **extra,
 | 
						|
        }
 | 
						|
 | 
						|
    _params_for_A = _params_for_multiple
 | 
						|
    _params_for_AAAA = _params_for_multiple
 | 
						|
 | 
						|
    def _params_for_NS(self, record):
 | 
						|
        return {
 | 
						|
            "ttl": record.ttl,
 | 
						|
            "resource_records": [
 | 
						|
                {"content": [value]} for value in record.values
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def _params_for_TXT(self, record):
 | 
						|
        return {
 | 
						|
            "ttl": record.ttl,
 | 
						|
            "resource_records": [
 | 
						|
                {"content": [value.replace("\\;", ";")]}
 | 
						|
                for value in record.values
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def _params_for_MX(self, record):
 | 
						|
        return {
 | 
						|
            "ttl": record.ttl,
 | 
						|
            "resource_records": [
 | 
						|
                {"content": [rec.preference, rec.exchange]}
 | 
						|
                for rec in record.values
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def _params_for_SRV(self, record):
 | 
						|
        return {
 | 
						|
            "ttl": record.ttl,
 | 
						|
            "resource_records": [
 | 
						|
                {"content": [rec.priority, rec.weight, rec.port, rec.target]}
 | 
						|
                for rec in record.values
 | 
						|
            ],
 | 
						|
        }
 | 
						|
 | 
						|
    def _apply_create(self, change):
 | 
						|
        self.log.info("creating: %s", change)
 | 
						|
        new = change.new
 | 
						|
        data = getattr(self, f"_params_for_{new._type}")(new)
 | 
						|
        self._client.record_create(
 | 
						|
            new.zone.name[:-1], new.fqdn, new._type, data
 | 
						|
        )
 | 
						|
 | 
						|
    def _apply_update(self, change):
 | 
						|
        self.log.info("updating: %s", change)
 | 
						|
        new = change.new
 | 
						|
        data = getattr(self, f"_params_for_{new._type}")(new)
 | 
						|
        self._client.record_update(
 | 
						|
            new.zone.name[:-1], new.fqdn, new._type, data
 | 
						|
        )
 | 
						|
 | 
						|
    def _apply_delete(self, change):
 | 
						|
        self.log.info("deleting: %s", change)
 | 
						|
        existing = change.existing
 | 
						|
        self._client.record_delete(
 | 
						|
            existing.zone.name[:-1], existing.fqdn, existing._type
 | 
						|
        )
 | 
						|
 | 
						|
    def _apply(self, plan):
 | 
						|
        desired = plan.desired
 | 
						|
        changes = plan.changes
 | 
						|
        zone = desired.name[:-1]
 | 
						|
        self.log.debug(
 | 
						|
            "_apply: zone=%s, len(changes)=%d", desired.name, len(changes)
 | 
						|
        )
 | 
						|
 | 
						|
        try:
 | 
						|
            self._client.zone(zone)
 | 
						|
        except GCoreClientNotFound:
 | 
						|
            self.log.info("_apply: no existing zone, trying to create it")
 | 
						|
            self._client.zone_create(zone)
 | 
						|
            self.log.info("_apply: zone has been successfully created")
 | 
						|
 | 
						|
        changes.reverse()
 | 
						|
 | 
						|
        for change in changes:
 | 
						|
            class_name = change.__class__.__name__
 | 
						|
            getattr(self, f"_apply_{class_name.lower()}")(change)
 |